Android高仿iOS Messages录音操作按钮

Android高仿iOS Messages录音操作按钮

前面的2次开发,分别完成了实现录音和播放以及相对应的波形。

  1. Android低仿iOS Messages录音波形效果
  2. Android高仿iOS Messages声音播放波形效果

这次的开发目标是解决录音操作按钮。

一、目标

实现录音操作按钮,为神马笔记实现录音功能做准备。

二、功能分析

iOS Messages录音按钮可以通过上下滑动选择不同的功能。

其中一个使用场景:

  • 长按录音按钮启动录音功能
  • 向上滑动到发送按钮,发送语音
  • 向下滑动到停止按钮,停止录音
  • 滑动到按钮外部,不进行任何操作
截图 描述
录音按钮及按下状态
发送按钮及按下状态
播放按钮及按下状态
暂停按钮及按下状态

三、实现效果

没有动态效果,看起来跟LinearLayout或者其他的控件容器毫无差别。

进行触控操作,则可以通过上下滑动选择不同的功能。

四、实现过程

因为布局方式与LinearLayout非常一致,因此选择LinearLayout为蓝本,然后重新编写触控事件。

触控事件的处理方式则参考ScrollView,因为无需处理滚动,实现非常之简单。

使用时,需要将子控件clickable属性设置为true,才能反应控件状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
public class ActionLayout extends LinearLayout {

private static final String TAG = "ActionLayout";

View actionView;
View.OnClickListener onClickListener;

/**
* Sentinel value for no current active pointer.
* Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;

/**
* ID of the active pointer. This is used to retain consistency during
* drags/flings if multiple pointers are used.
*/
private int mActivePointerId = INVALID_POINTER;

public ActionLayout(Context context) {
this(context, null);
}

public ActionLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public ActionLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public ActionLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

@Override
public void setOnClickListener(@Nullable OnClickListener l) {
this.onClickListener = l;

super.setOnClickListener(l);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
super.onInterceptTouchEvent(ev);

final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);

this.updateAction(ev);

break;
}
case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == INVALID_POINTER) {
mActivePointerId = ev.getPointerId(0);
}

this.updateAction(ev);

break;
}
case MotionEvent.ACTION_CANCEL: {

this.mActivePointerId = INVALID_POINTER;
this.actionView = null;

break;
}

case MotionEvent.ACTION_UP: {

this.fireAction();

this.mActivePointerId = INVALID_POINTER;
this.actionView = null;

break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);

this.updateAction(ev);

break;
}
}

return isClickable() || (getChildCount() != 0);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {

final int actionMasked = ev.getActionMasked();

switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);

updateAction(ev);

break;
}

case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == INVALID_POINTER) {
mActivePointerId = ev.getPointerId(0);
}

updateAction(ev);

break;
}

case MotionEvent.ACTION_UP: {

fireAction();

this.mActivePointerId = INVALID_POINTER;
this.actionView = null;

break;
}

case MotionEvent.ACTION_CANCEL: {

mActivePointerId = INVALID_POINTER;
this.actionView = null;

break;
}

case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
this.mActivePointerId = ev.getPointerId(index);

this.updateAction(ev);

break;
}

case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);

this.updateAction(ev);

break;
}
}

return isClickable() || (getChildCount() != 0);
}

void fireAction() {
if (actionView == null) {
return;
}

actionView.setPressed(false);
if (onClickListener != null) {
onClickListener.onClick(actionView);
}
}

void updateAction(MotionEvent ev) {
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
return;
}

final float x = ev.getX(activePointerIndex);
final float y = ev.getY(activePointerIndex);

View view = findChildViewUnder(x, y);
if (view != null && view.getVisibility() != View.VISIBLE) {
view = null;
}

if (actionView == view) {
return;
}

if (actionView != null) {
actionView.setPressed(false);
}

if (view != null) {
view.setPressed(true);
}

this.actionView = view;
}

View findChildViewUnder(float x, float y) {
final int count = this.getChildCount();
for (int i = count - 1; i >= 0; i--) {
View child = this.getChildAt(i);

final float translationX = child.getTranslationX();
final float translationY = child.getTranslationY();
if (x >= child.getLeft() + translationX
&& x <= child.getRight() + translationX
&& y >= child.getTop() + translationY
&& y <= child.getBottom() + translationY) {
return child;
}
}

return null;
}

void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
}

五、开发过程回顾

明确开发目标,选择LinearLayoutScrollView为蓝本进行改造。

还从RecyclerView拿了点代码来用。

六、接下来

继续朝录音编辑器迈进。

七、Finally

尔时。世尊而说偈言。

若以色见我。 以音声求我。 是人行邪道。 不能见如来。