[译] Android 为 View、ViewGroup 添加前景色

很多人都知道 Android 的卡片式点击效果,这个 selector 是画在前景上的而不是背景上的,虽然我们一般都是用背景色。这个点击效果实现方式也并不难,下面就给出几种情况下的方法。

下图是 Google Play 应用中按压效果的一个截图:

若为 FrameLayout

添加前景色会非常简单,因为 FrameLayout 本身就有这样一个方法叫 setForeground。实际上,只需要通过在 XML 中android:foreground或代码中动态传一个 selector 作为参数,然后调用 setForeground 就可以了。

若不为 FrameLayout

别担心,这也不会很难。我们只要设置给 selector 设置合适的状态(pressed, focused …),然后设置边界并且在视图画好自己后再画它。这样,selector 就会覆盖在视图之上。

  • Changing the State

在 View 这个类,每次视图改变自己的 drawable state 时都会调用的方法:drawableStateChanged

1
2
3
4
5
6
7
8
9
@Override
protected void drawableStateChanged () {
super.drawableStateChanged();
mForegroundSelector.setState(getDrawableState());
//redraw
invalidate();
}
  • Updating the Drawable Bounds

每次视图改变大小时都会调用的方法:onSizeChanged

1
2
3
4
5
6
@Override
protected void onSizeChanged (int width, int height, int oldwidth, int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
mForegroundSelector.setBounds(0, 0, width, height);
}
  • Drawing the Selector

分为两种情况:

(1) 视图不是 ViewGroup

必须在调用父类的 onDraw(Canvas canvas) 之后再画 selector。

1
2
3
4
5
6
@Override
protected void onDraw (Canvas canvas) {
super.onDraw(canvas);
mForegroundSelector.draw(canvas);
}

(2) 视图 ViewGroup

必须在画好所有的子视图之后在画,也就是说在调用 dispatchDraw(Canvas canvas) 之后。

1
2
3
4
5
6
@Override
protected void dispatchDraw (Canvas canvas) {
super.dispatchDraw(canvas);
mForegroundSelector.draw(canvas);
}
  • Drawing an Animated Drawable

如果你的 drawable 是动画类型的,那就还有些事情需要做。假设我们有一个有android:exitFadeDuration属性的 selector,也就是说当 selector 改变它的状态的时候,旧的状态会慢慢淡出。

首先,我们需要在 onDraw() 或者 dispatchDraw() 方法中添加 selector 的 draw(Canvas) 方法,如下:

1
2
3
4
5
6
@Override
protected void onDraw (Canvas canvas) {
super.onDraw(canvas);
mForegroundSelector.draw(canvas);
}

然后,我们要重写 jumpDrawablesToCurrentState 方法来通知 selector 去做状态之间的转换动画,重写 verifyDrawable 方法来告诉视图我们在显示自己的 drawable。

1
2
3
4
5
6
7
8
9
10
11
@Override
protected boolean verifyDrawable (Drawable who) {
return super.verifyDrawable(who) || (who == mForegroundDrawable);
}
@TargetApi(11)
@Override
public void jumpDrawablesToCurrentState () {
super.jumpDrawablesToCurrentState();
mForegroundSelector.jumpToCurrentState();
}

但是,jumpToCurrentState 方法里面做了什么呢?我们来 DrawableContainer 类中的一点源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void jumpToCurrentState () {
boolean changed = false;
if (mLastDrawable != null) {
mLastDrawable.jumpToCurrentState();
mLastDrawable = null;
changed = true;
}
if (mCurrDrawable != null) {
mCurrDrawable.jumpToCurrentState();
mCurrDrawable.mutate().setAlpha(mAlpha);
}
if (mExitAnimationEnd != 0) {
mExitAnimationEnd = 0;
changed = true;
}
if (mEnterAnimationEnd != 0) {
mEnterAnimationEnd = 0;
changed = true;
}
if (changed) {
invalidateSelf();
}
}

我们看到 jumpToCurrentState 中会调用 invalidateSelf,下面再看下 invalidateSelf 方法的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Use the current {@link Callback} implementation to have this Drawable
* redrawn. Does nothing if there is no Callback attached to the
* Drawable.
*
* @see Callback#invalidateDrawable
* @see #getCallback()
* @see #setCallback(android.graphics.drawable.Drawable.Callback)
*/
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}

可以看到如果 callback 为空,就不会重绘 drawable。所以我们在初始化 selector 时要设置 callback。

1
2
3
4
5
6
private void init(Context context) {
mForegroundDrawable = getResources().getDrawable(R.drawable.myselector);
//set a callback, or the selector won't be animated
mForegroundDrawable.setCallback(this);
}

现在你的 selector 就有淡出动画效果了!

EXTRA

  • 把默认的背景设为前景

你可以获得你主题中的默认背景的 selector,然后把它设为前景的 selector。

1
2
3
4
5
6
TypedArray a = getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground});
mForegroundDrawable = a.getDrawable(0);
if (mForegroundDrawable != null) {
mForegroundDrawable.setCallback(this);
}
a.recycle();

原文链接:DZone 作者:Antoine Merle

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!