GlowPadView是通话界面中来电所用的控件,比较复杂。已有人写过Android 4.2 关于GlowPadView的说明,介绍了xml中的各种属性,并贴图详细解释,强烈推荐看本文前先看链接中的文章。
packages/apps/InCallUI/src/com/android/incallui/widget/multiwaveview下都是GlowPadView相关的文件
动画
Ease.java
static class Linear {
public static final TimeInterpolator easeNone = new TimeInterpolator() {
public float getInterpolation(float input) {
return input;
}
};
}
static class Cubic {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*(input/=DURATION)*input*input + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return ((input/=DURATION/2) < 1.0f) ?
(DOMAIN/2*input*input*input + START)
: (DOMAIN/2*((input-=2)*input*input + 2) + START);
}
};
}
Ease中定义了多种Interpolator,控制动画执行变化率。
Tweener.java
主要用于存储多个动画
ObjectAnimator animator;
private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>();
Tweener成员就是一个animator,并有一个静态HashMap存储多个Tweener,该类中最重要的是生成Tweener的to方法:
public static Tweener to(Object object, long duration, Object... vars) { //Object为拥有属性值的对象,duration是时长
long delay = 0;
AnimatorUpdateListener updateListener = null;
AnimatorListener listener = null;
TimeInterpolator interpolator = null;
// Iterate through arguments and discover properties to animate
ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2);
for (int i = 0; i < vars.length; i+=2) { //vars必须是双数,每一对是key和values,每一对都能生成动画的一部分
if (!(vars[i] instanceof String)) {
throw new IllegalArgumentException("Key must be a string: " + vars[i]);
}
String key = (String) vars[i];
Object value = vars[i+1];
if ("simultaneousTween".equals(key)) {
// TODO
} else if ("ease".equals(key)) { //key为ease的话是设置interpolator
interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
} else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
updateListener = (AnimatorUpdateListener) value; //key为onUpdate是设置updateListener
} else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) {
listener = (AnimatorListener) value; //key为onComplete是设置动画结束的listener
} else if ("delay".equals(key)) {
delay = ((Number) value).longValue(); //key为delay是设置动画延时
} else if ("syncWith".equals(key)) {
// TODO
} else if (value instanceof float[]) { //value为float数组则加入一个float动画属性
props.add(PropertyValuesHolder.ofFloat(key,
((float[])value)[0], ((float[])value)[1]));
} else if (value instanceof int[]) { //value为int数组则加入一个int动画属性
props.add(PropertyValuesHolder.ofInt(key,
((int[])value)[0], ((int[])value)[1]));
} else if (value instanceof Number) { value为数字包装类型,例如Integer等则统一按float动画属性处理
float floatValue = ((Number)value).floatValue();
props.add(PropertyValuesHolder.ofFloat(key, floatValue));
} else {
throw new IllegalArgumentException(
"Bad argument for key \"" + key + "\" with value " + value.getClass());
}
}
// Re-use existing tween, if present
Tweener tween = sTweens.get(object);
ObjectAnimator anim = null;
if (tween == null) { //HashMap中没有取到动画则新建Tweener
anim = ObjectAnimator.ofPropertyValuesHolder(object,
props.toArray(new PropertyValuesHolder[props.size()]));
tween = new Tweener(anim);
sTweens.put(object, tween); //存储到HashMap中
if (DEBUG) Log.v(TAG, "Added new Tweener " + tween);
} else {
anim = sTweens.get(object).animator; //已有动画则替换动画
replace(props, object); // Cancel all animators for given object
}
if (interpolator != null) { //后续都是设置动画listener、interpolator等
anim.setInterpolator(interpolator);
}
// Update animation with properties discovered in loop above
anim.setStartDelay(delay);
anim.setDuration(duration);
if (updateListener != null) {
anim.removeAllUpdateListeners(); // There should be only one
anim.addUpdateListener(updateListener);
}
if (listener != null) {
anim.removeAllListeners(); // There should be only one.
anim.addListener(listener);
}
anim.addListener(mCleanupListener);
return tween;
}
该方法也是静态的,这样外界使用Tweener就是通过这一个方法。可以看出Tweener没有添加什么功能,唯一作用是Tweener生成动画的代码片段明显比手动生成一个动画简洁好多,尤其是GlowPadView中有很多用动画的地方,截取一个代码片段如下:
mTargetAnimations.add(Tweener.to(target, duration,
"ease", interpolator,
"alpha", 0.0f,
"scaleX", targetScale,
"scaleY", targetScale,
"delay", delay,
"onUpdate", mUpdateListener));
AnimationBundle
private class AnimationBundle extends ArrayList<Tweener> {
private static final long serialVersionUID = 0xA84D78726F127468L;
private boolean mSuspended;
public void start() {
if (mSuspended) return; // ignore attempts to start animations
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.start();
}
}
public void cancel() {
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.cancel();
}
clear();
}
public void stop() {
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.end();
}
clear();
}
public void setSuspended(boolean suspend) {
mSuspended = suspend;
}
};
这个是GlowPadView的内部类,继承ArrayList<Tweener>就能看出该类作用是统一管理一组动画。
private AnimationBundle mWaveAnimations = new AnimationBundle();
private AnimationBundle mTargetAnimations = new AnimationBundle();
private AnimationBundle mGlowAnimations = new AnimationBundle();
有三个AnimationBundle成员,分别代表来电未按下动画,按下或放开瞬间的动画,手指头按下点PointCloud的动画。
mWaveAnimations
private void startWaveAnimation() {
mWaveAnimations.cancel();
mPointCloud.waveManager.setAlpha(1.0f);
mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
"ease", Ease.Quad.easeOut,
"delay", 0,
"radius", 2.0f * mOuterRadius,
"onUpdate", mUpdateListener,
"onComplete",
new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animator) {
mPointCloud.waveManager.setRadius(0.0f);
mPointCloud.waveManager.setAlpha(0.0f);
}
}));
mWaveAnimations.start();
}
PointCloud是从一个小圆扩散到大圆的点状动画。要变化的对象是mPointCloud.waveManager,它是PointCloud的内部类,定义了半径和alpha变量,然后在绘制方法draw中使用这些内部类变量,这个类是专门为动画而设计的;radius是绘制范围半径,从mHandleDrawable.getWidth()/2.0f->2.0f * mOuterRadius;ease的值就是取之前介绍的Ease中的常量;动画结束后设置半径为0、alpha为0。
注意来电时候这个动画是不断循环运行的,那么循环就是GlowPadWrapper中的triggerPing实现
private void triggerPing() {
Log.d(this, "triggerPing(): " + mPingEnabled + " " + this);
if (mPingEnabled && !mPingHandler.hasMessages(PING_MESSAGE_WHAT)) {
ping();
if (ENABLE_PING_AUTO_REPEAT) {
mPingHandler.sendEmptyMessageDelayed(PING_MESSAGE_WHAT, PING_REPEAT_DELAY_MS);
}
}
}
private final Handler mPingHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PING_MESSAGE_WHAT:
triggerPing();
break;
}
}
};
triggerPing中调用ping发起动画并延时发送一个消息,该消息的处理中又会调用triggerPing,这样就实现了无限循环。
public void ping() {
if (mFeedbackCount > 0) {
boolean doWaveAnimation = true;
final AnimationBundle waveAnimations = mWaveAnimations;
// Don't do a wave if there's already one in progress
if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
long t = waveAnimations.get(0).animator.getCurrentPlayTime();
if (t < WAVE_ANIMATION_DURATION/2) {
doWaveAnimation = false;
}
}
if (doWaveAnimation) {
startWaveAnimation(); //开启动画
}
}
}
mTargetAnimations
这个包括两部分,一个是显示,另外一个是隐藏。
private void hideTargets(boolean animate, boolean expanded) {
mTargetAnimations.cancel();
// Note: these animations should complete at the same time so that we can swap out
// the target assets asynchronously from the setTargetResources() call.
mAnimatingTargets = animate;
final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
final float targetScale = expanded ?
TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
final int length = mTargetDrawables.size();
final TimeInterpolator interpolator = Ease.Cubic.easeOut;
for (int i = 0; i < length; i++) { //循环中初始每个target的动画
TargetDrawable target = mTargetDrawables.get(i);
target.setState(TargetDrawable.STATE_INACTIVE);
mTargetAnimations.add(Tweener.to(target, duration,
"ease", interpolator,
"alpha", 0.0f, //value只有1个的话,表示动画是从当前的值变化到指定值,alpha从1->0
"scaleX", targetScale, //scale从1->0.8
"scaleY", targetScale,
"delay", delay,
"onUpdate", mUpdateListener));
}
float ringScaleTarget = expanded ?
RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
ringScaleTarget *= mRingScaleFactor;
mTargetAnimations.add(Tweener.to(mOuterRing, duration, //外部大圆环动画
"ease", interpolator,
"alpha", 0.0f,
"scaleX", ringScaleTarget,
"scaleY", ringScaleTarget,
"delay", delay,
"onUpdate", mUpdateListener,
"onComplete", mTargetUpdateListener));
mTargetAnimations.start();
}
private void showTargets(boolean animate) {
...
}
两个动画相当于是相反的关系,介绍一个就够了。target就是按下时显示的接听、拒接及短信拒接按钮,动画同时包括了alpha和大小的变化,隐藏时是大小由1->0.8、alpha由1->0,就是视觉上缩小并消失的过程,显示的效果正好相反。除此之外还有个外层大圆环的动画,动画效果一样,这个大圆环显示了控件触摸的边界。
mGlowAnimations
这个动画在按下后选中某个target时显示,也包括显示和隐藏两个动画 private void showGlow(int duration, int delay, float finalAlpha,
AnimatorListener finishListener) {
mGlowAnimations.cancel();
mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
"ease", Ease.Cubic.easeIn,
"delay", delay,
"alpha", finalAlpha,
"onUpdate", mUpdateListener,
"onComplete", finishListener));
mGlowAnimations.start();
}
private void hideGlow(int duration, int delay, float finalAlpha,
AnimatorListener finishListener) {
...
}
这里的mPointCloud.glowManager是PointCloud中另外一个内部类,除了alpha,半径外还包含了坐标x和y。两个方法调用的时候finalAlpha所赋的值都是0.0f,这个很奇怪,这样两个方法其实都是消失动画呀。还是有不同的,首先是动画变化速率不一样,其次是hideGlow还包含坐标的变化(动画结束后坐标恢复为0.0,这样方便运行mWaveAnimations )。showGlow其实的含义是showTarget,是选中某个target后突出显示target,然后表示手指所按位置的点状图消失,其实就是个PointCloud消失的动画。
背景动画
private Tweener mBackgroundAnimator;
private void startBackgroundAnimation(int duration, float alpha) {
final Drawable background = getBackground();
if (mAlwaysTrackFinger && background != null) {
if (mBackgroundAnimator != null) {
mBackgroundAnimator.animator.cancel();
}
mBackgroundAnimator = Tweener.to(background, duration,
"ease", Ease.Cubic.easeIn,
"alpha", (int)(255.0f * alpha),
"delay", SHOW_ANIMATION_DELAY);
mBackgroundAnimator.animator.start();
}
}
背景alpha的动画,代码中未按下时背景的alpha值是0,按下后alpha值是1,因为需要背景来衬托接听、挂断等按键的显示。
TargetDrawable
GlowPadView有多个TargetDrawable成员变量
private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); //按下后接听、挂断等图标
private TargetDrawable mHandleDrawable; //未接听时正中间显示的图标
private TargetDrawable mOuterRing; //显示触摸边界的大圆环
初始化和位置设置
mTargetDrawables 包含了按下后所有的可选项。在构造方法中依据xml值初始化 if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
internalSetTargetResources(outValue.resourceId);
}
当然还可以重新设置:
public void setTargetResources(int resourceId) {
if (mAnimatingTargets) {
// postpone this change until we return to the initial state
mNewTargetResources = resourceId;
} else {
internalSetTargetResources(resourceId);
}
}
重新设置的情况有几种,例如视频通话和语音通话的切换、对方开启主叫隐藏后无法短信拒接等。
private void internalSetTargetResources(int resourceId) {
final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId); //依据资源id创建对象
mTargetDrawables = targets;
mTargetResourceId = resourceId;
int maxWidth = mHandleDrawable.getWidth();
int maxHeight = mHandleDrawable.getHeight();
final int count = targets.size();
for (int i = 0; i < count; i++) {
TargetDrawable target = targets.get(i);
maxWidth = Math.max(maxWidth, target.getWidth());
maxHeight = Math.max(maxHeight, target.getHeight());
}
if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
mMaxTargetWidth = maxWidth;
mMaxTargetHeight = maxHeight;
requestLayout(); // required to resize layout and call updateTargetPositions()
} else {
updateTargetPositions(mWaveCenterX, mWaveCenterY); //设置位置
updatePointCloudPosition(mWaveCenterX, mWaveCenterY); //设置PointCloud位置
}
}
下面看位置设置
private void updateTargetPositions(float centerX, float centerY) {
// Reposition the target drawables if the view changed.
ArrayList<TargetDrawable> targets = mTargetDrawables;
final int size = targets.size();
final float alpha = (float) (-2.0f * Math.PI / size);
for (int i = 0; i < size; i++) {
final TargetDrawable targetIcon = targets.get(i);
final float angle = alpha * i;
targetIcon.setPositionX(centerX);
targetIcon.setPositionY(centerY);
targetIcon.setX(getRingWidth() / 2 * (float) Math.cos(angle));
targetIcon.setY(getRingHeight() / 2 * (float) Math.sin(angle));
}
}
size都是4,所以angel是90度,所以target都是间隔直角。那么两或者三个选项怎么也是90度?见xml中定义:
<array name="incoming_call_widget_audio_without_sms_targets">
<item>@drawable/ic_lockscreen_answer</item>
<item>@null</item>
<item>@drawable/ic_lockscreen_decline</item>
<item>@null</item>"
</array>
资源数组用空项占位置,所以所有情况下间隔角度都是90度。
状态切换
mTargetDrawables 选中和未选中显示的图片是不一样的,这个实现比较简单
public static final int[] STATE_ACTIVE =
{ android.R.attr.state_enabled, android.R.attr.state_active };
public static final int[] STATE_INACTIVE =
{ android.R.attr.state_enabled, -android.R.attr.state_active };
public static final int[] STATE_FOCUSED =
{ android.R.attr.state_enabled, -android.R.attr.state_active,
android.R.attr.state_focused };
TargetDrawble类中定义了三个状态。
public void setState(int [] state) {
if (mDrawable instanceof StateListDrawable) {
StateListDrawable d = (StateListDrawable) mDrawable;
d.setState(state);
}
}
实用setState即可切换图片,当然mDrawable要是StateListDrawable图片,例如接听按键图片
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_enabled="true" android:state_active="false" android:state_focused="false"
android:drawable="@drawable/ic_lockscreen_answer_normal_layer"/>
<item
android:state_enabled="true" android:state_active="true" android:state_focused="false"
android:drawable="@drawable/ic_lockscreen_answer_activated_layer" />
<item
android:state_enabled="true" android:state_active="false" android:state_focused="true"
android:drawable="@drawable/ic_lockscreen_answer_activated_layer" />
</selector>
选中确认
private void handleMove(MotionEvent event) {
...
final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
final float snapDistance2 = snapRadius * snapRadius;
// Find first target in range
for (int i = 0; i < ntargets; i++) {
TargetDrawable target = targets.get(i);
double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets;
double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets;
if (target.isEnabled()) {
boolean angleMatches =
(angleRad > targetMinRad && angleRad <= targetMaxRad) ||
(angleRad + 2 * Math.PI > targetMinRad &&
angleRad + 2 * Math.PI <= targetMaxRad);
if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
activeTarget = i;
}
}
}
...
}
handleMove中处理mTargetDrawables 选中与否。ntargets介绍过永远是4,所以范围在正负45度之间且离原点的距离大于snapDistance2即是选中状态。
mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); //构造方法中初始化
这个距离是可以在xml中配置的。
控件绘制
@Override
protected void onDraw(Canvas canvas) {
mPointCloud.draw(canvas);
mOuterRing.draw(canvas);
final int ntargets = mTargetDrawables.size();
for (int i = 0; i < ntargets; i++) {
TargetDrawable target = mTargetDrawables.get(i);
if (target != null) {
target.draw(canvas);
}
}
mHandleDrawable.draw(canvas);
}
绘制统一在onDraw中进行,绘制的内容有mPointCloud、mOuterRing,mTargetDrawables和mHandleDrawable。可见所有东西都是绘制在屏幕上的,只是在不同的状态下alpha值不一样。
private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
};
因为这四个东西是通过OnDraw绘制的,所以相关动画中的updateListener都是mUpdateListener,动作只有invalidate()一个,触发控件的重新绘制。
控件状态切换
// Wave state machine
private static final int STATE_IDLE = 0; //空闲态
private static final int STATE_START = 1; //开始按下,不过一般会马上切换到STATE_FIRST_TOUCH
private static final int STATE_FIRST_TOUCH = 2; //第一次按下后还没有移动
private static final int STATE_TRACKING = 3; //按下移动后但是没有target被选中
private static final int STATE_SNAP = 4; //按下移动后有target被选中
private static final int STATE_FINISH = 5; //手指抬起,在隐藏动画结束后会切换到STATE_IDLE
有五个状态,设置状态的方法是switchToState
private void switchToState(int state, float x, float y) {
switch (state) {
case STATE_IDLE: //隐藏mTargetDrawables,mPointCloud,mHandleDrawable
deactivateTargets();
/// M: need to hide target when change to idle state. @{
hideTargets(false, false);
/// @}
hideGlow(0, 0, 0.0f, null);
startBackgroundAnimation(0, 0.0f);
mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
mHandleDrawable.setAlpha(1.0f);
break;
case STATE_START:
startBackgroundAnimation(0, 0.0f);
break;
case STATE_FIRST_TOUCH: //隐藏mHandleDrawable,显示mTargetDrawables,设置背景alpha为1
mHandleDrawable.setAlpha(0.0f);
deactivateTargets();
showTargets(true);
startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
setGrabbedState(OnTriggerListener.CENTER_HANDLE);
...
break;
case STATE_TRACKING:
mHandleDrawable.setAlpha(0.0f);
break;
case STATE_SNAP:
// TODO: Add transition states (see list_selector_background_transition.xml)
mHandleDrawable.setAlpha(0.0f);
showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
break;
case STATE_FINISH: //做隐藏动画,动画结束后切换到idle状态。
doFinish();
break;
}
}
引起状态变化的是手指的动作,见
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
boolean handled = false;
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
if (DEBUG) Log.v(TAG, "*** DOWN ***");
handleDown(event);
handleMove(event);
handled = true;
break;
case MotionEvent.ACTION_MOVE:
if (DEBUG) Log.v(TAG, "*** MOVE ***");
handleMove(event);
handled = true;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
if (DEBUG) Log.v(TAG, "*** UP ***");
handleMove(event);
handleUp(event);
handled = true;
break;
case MotionEvent.ACTION_CANCEL:
if (DEBUG) Log.v(TAG, "*** CANCEL ***");
handleMove(event);
handleCancel(event);
handled = true;
break;
}
invalidate();
return handled ? true : super.onTouchEvent(event);
}
通过handleDown,handleMove,handleUp和handleCancel四个方法引发状态的切换。
控件事件触发
GlowPadView中定义了接口OnTriggerListener:
public interface OnTriggerListener {
int NO_HANDLE = 0;
int CENTER_HANDLE = 1;
public void onGrabbed(View v, int handle);
public void onReleased(View v, int handle);
public void onTrigger(View v, int target);
public void onGrabbedStateChange(View v, int handle);
public void onFinishFinalAnimation();
}
GlowPadWrapper实现了该接口并且赋值给GlowPadView中的mOnTriggerListener,其中onFinishFinalAnimation和onGrabbedStateChange函数体为空,onGrabbed和onReleased开始或者结束mWaveAnimations的动画,最重要的是onTrigger
public void onTrigger(View v, int target) {
Log.d(this, "onTrigger() view=" + v + " target=" + target);
final int resId = getResourceIdForTarget(target);
switch (resId) {
case R.drawable.ic_lockscreen_answer: //接听来电
mAnswerListener.onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext());
mTargetTriggered = true;
break;
case R.drawable.ic_lockscreen_decline: //挂断来电
mAnswerListener.onDecline(getContext());
mTargetTriggered = true;
break;
case R.drawable.ic_lockscreen_text: //短信拒接
mAnswerListener.onText();
mTargetTriggered = true;
break;
...
default:
// Code should never reach here.
Log.e(this, "Trigger detected on unhandled resource. Skipping.");
}
}
其中mAnswerListener是个接口,定义在GlowPadWrapper中
public interface AnswerListener {
void onAnswer(int videoState, Context context);
void onDecline(Context context);
void onDeclineUpgradeRequest(Context context);
void onText();
}
实现是AnswerFragment,继而调用AnswerPresenter,然后继续往下调用TelecomAdapter,TelecomAdapter再往下已经超出InCallUI的代码范围了。
其它
还有一个成员
private GlowpadExploreByTouchHelper mExploreByTouchHelper;
是无障碍辅助服务相关,这个实现有需要的自己分析,反正我是没见过有任何一个产品经理讨论过Android的AccessibilityService,我猜知道这个的人都不多。