今天在弄一个小东西的时候用到StateListDrawable,结果遇到一点问题。现在简单总结一下。
问题是这样的,在前面写SegmentView的时候,给里面的各个Item添加背景,想着中间的几个Item(中间的Item都没有圆角)的背景都是一样的,那就用一个StateListDrawable对象就够了吧。结果就出问题了,如果中间有2个或更多的Item,当点击它们的时候背景就乱了,和想象的结果完全不一样。下面我把代码贴出来:
package com.belows.test;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private StateListDrawable mListDrawable;
private ColorStateList mColorList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int selected = android.R.attr.state_selected;
mListDrawable = new StateListDrawable();
ColorDrawable selectedDrawable = new ColorDrawable(Color.BLUE);
ColorDrawable normalDrawable = new ColorDrawable(Color.YELLOW);
mListDrawable.addState(new int[]{selected}, selectedDrawable);
mListDrawable.addState(new int[]{-selected}, normalDrawable);
mColorList = new ColorStateList(new int[][]{{selected},{-selected}},new int[]{Color.WHITE,Color.GRAY});
setContentView(genRootView());
}
private LinearLayout genRootView() {
final LinearLayout root = new LinearLayout(this);
root.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dip2px(this, 50)));
root.addView(genTabView("Tab1"));
root.addView(genTabView("Tab2"));
root.addView(genTabView("Tab3"));
root.addView(genTabView("Tab4"));
for (int i=0,size=root.getChildCount(); i<size; ++i) {
root.getChildAt(i).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectItem(root,v);
}
});
}
return root;
}
private void selectItem(LinearLayout root, View selectedView) {
for (int i=0,size=root.getChildCount(); i<size; ++i) {
root.getChildAt(i).setSelected(false);
}
selectedView.setSelected(true);
}
private TextView genTabView(String tab) {
TextView textView = new TextView(this);
textView.setText(tab);
textView.setBackgroundDrawable(mListDrawable);
textView.setTextColor(mColorList);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
lp.weight = 1;
textView.setLayoutParams(lp);
return textView;
}
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
}
大家可以看到点击各个Tab后看到的现象非常奇怪,各个Tab背景的转换非常混乱,甚至都没有什么规律可言,不过Text的颜色(textColor)却是按照设想的方式在正常工作,这是怎么一回事呢?
还是到源码里去看看吧,首先看看setSelected(boolean selected)这个函数
public void setSelected(boolean selected) {
//noinspection DoubleNegation
if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {
mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);
if (!selected) resetPressedState();
invalidate(true);
refreshDrawableState();
dispatchSetSelected(selected);
if (selected) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
} else {
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
函数一开始就判断设置的select状态是否和View当前的select状态一样,如果是一样的话就不做任何操作了
if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected)
如果不一样,就先设置选中的状态,然后刷新View,再然后调用refreshDrawableState();这个函数,而这个函数又会调用drawableStateChanged(),看一下这个函数做了什么
protected void drawableStateChanged() {
final int[] state = getDrawableState();
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
bg.setState(state);
}
final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (fg != null && fg.isStateful()) {
fg.setState(state);
}
if (mScrollCache != null) {
final Drawable scrollBar = mScrollCache.scrollBar;
if (scrollBar != null && scrollBar.isStateful()) {
scrollBar.setState(state);
}
}
if (mStateListAnimator != null) {
mStateListAnimator.setState(state);
}
}
可以从中看到当我们设置View的selected状态时,View会将这个状态传递给background,foreground等几个Drawable。
在这个例子中,由于设置View的background时用的都是同一个Drawable,当其中的一个Tab设置selected为true时,其backgroundDrawable的state也被设置为true,这时其他的backgroundDrawable的state也跟着改变了,所以就乱了。
从效果图可以看到,当一开始点击第一个Tab时,第四个Tab的背景改变了,而第二、三个没有改变,这又是怎么会事呢?首先我们看一下setBackgroundDrawable这个函数,
@Deprecated
public void setBackgroundDrawable(Drawable background) {
...
background.setCallback(this);
...
invalidate(true);
}
我们看到在这个函数里,我们的Drawable的callback是调用这个函数的View,所以当四个View都调用了setBackgroundDrawable之后,这个Drawable的callback就是第四个View了。然后我们再来看一下View的setSelected函数,通过一系列的跳转(setSelected->refreshDrawableState->drawableStateChanged->(StateListDrawable)setState->onStateChange->selectDrawable->invalidateSelf)到了StateListDrawable的invalidateSelf函数,我们来看一下这个函数:
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
这里的callback就是我们刚才调用setBackgroundDrawable时设置的callback,也就是第四个View,而View的invalidateDrawable函数又做了什么呢?
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
其中getDirtyBounds其实就是Drawable的整个区域(这个我们可以查看Drawable的源码看到,StateListDrawable没有复写这个方法),所以当我们调用setSelect方法的时候其实是被选中的那个View自己在刷新,但是由于Drawable的callback是第四个View,所以第四个View的背景也跟着刷新了。