在定义ListView的Selector时候,有个drawSelectorOnTop的属性,如果drawSelectorOnTop为true的话,Selector的效果是画在List Item的上面(Selector是盖住了ListView的文字或者图片),即Foreground前景。如果drawSelectorOnTop为false的话,Selector的效果是画在List Item的下面,即Background背景。由于项目中恰好需要自定义View,需要实现此效果。
本文借ListView的代码来剖析一下,
ListView完成此部分功能在frameworks\base\core\java\android\widget\AbsListView.java文件中。
用mSelector即ListView要画的Selector(资源文件),而mSelectorRect则是想要画的区域。
/** * Indicates whether the list selector should be drawn on top of the children or behind */ boolean mDrawSelectorOnTop = false; 决定画前景还是背景 /** * The drawable used to draw the selector */ Drawable mSelector; ListView用中来显示Selector的Drawable,即ListSelector对应的XML文件 /** * The current position of the selector in the list. */ int mSelectorPosition = INVALID_POSITION; /** * Defines the selector's location and dimension at drawing time */ Rect mSelectorRect = new Rect(); 用来画Selector的区域,即Selector画的位置
AbsListView中构造方法中有获取selector
Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector); if (d != null) { setSelector(d); } //默认为false,画的是背景 mDrawSelectorOnTop = a.getBoolean( com.android.internal.R.styleable.AbsListView_drawSelectorOnTop, false);下面看一下setSelector是如何实现的
/** * Controls whether the selection highlight drawable should be drawn on top of the item or * behind it. * * @param onTop If true, the selector will be drawn on the item it is highlighting. The default * is false. * * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop */ public void setDrawSelectorOnTop(boolean onTop) { //提供是否画前景或者背景的接口 mDrawSelectorOnTop = onTop; } /** * Set a Drawable that should be used to highlight the currently selected item. * * @param resID A Drawable resource to use as the selection highlight. * * @attr ref android.R.styleable#AbsListView_listSelector */ public void setSelector(int resID) { setSelector(getResources().getDrawable(resID)); 设置listSelector的XML文件 } public void setSelector(Drawable sel) { if (mSelector != null) { mSelector.setCallback(null); unscheduleDrawable(mSelector); } mSelector = sel; Rect padding = new Rect(); sel.getPadding(padding); mSelectionLeftPadding = padding.left; mSelectionTopPadding = padding.top; mSelectionRightPadding = padding.right; mSelectionBottomPadding = padding.bottom; sel.setCallback(this); //需要给Selector设置Callback updateSelectorState(); } /** * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the * selection in the list. * * @return the drawable used to display the selector */ public Drawable getSelector() { return mSelector; } void updateSelectorState() { if (mSelector != null) { if (shouldShowSelector()) { mSelector.setState(getDrawableState());//更新Selector的状态 } else { mSelector.setState(StateSet.NOTHING); } } }
这样就将Selector设置给ListView了,并且更新了drawable的状态。
接下来我们再看一下Android是如何将drawable画到ListView的Item上的。
在AbsListView中有个onTouchEvent的方法用来处理Touch事件,其中有一段代码就是确定Selector要画的区域。
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { mTouchMode = TOUCH_MODE_TAP; setSelectedPositionInt(mMotionPosition); layoutChildren(); child.setPressed(true);//设置List Item状态为 pressed positionSelector(mMotionPosition, child);//确定画Selector的区域 setPressed(true); //设置ListView 的状态为pressed if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); if (!mDataChanged) { performClick.run(); } } }; postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; updateSelectorState(); } return true; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { performClick.run(); } }
接下来看看positionSelector的实现,
void positionSelector(int position, View sel) { if (position != INVALID_POSITION) { mSelectorPosition = position; } //设置Selector的区域为List Item View的边界 final Rect selectorRect = mSelectorRect; selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); if (sel instanceof SelectionBoundsAdjuster) { ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect); } positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, selectorRect.bottom); final boolean isChildViewEnabled = mIsChildViewEnabled; if (sel.isEnabled() != isChildViewEnabled) { mIsChildViewEnabled = !isChildViewEnabled; if (getSelectedItemPosition() != INVALID_POSITION) { refreshDrawableState();//根据View状态更新drawable的状态 } } } private void positionSelector(int l, int t, int r, int b) { mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r + mSelectionRightPadding, b + mSelectionBottomPadding); }
好了现在已经决定了将selector画在哪里,Selector的状态也已经更新OK。
还差一步没有做,那就是到底是将其怎么画上面的呢?
答案就在AbsListView.java里的dispatchDraw方法里面。
@Override protected void dispatchDraw(Canvas canvas) { int saveCount = 0; final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { saveCount = canvas.save(); final int scrollX = mScrollX; final int scrollY = mScrollY; canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, scrollX + mRight - mLeft - mPaddingRight, scrollY + mBottom - mTop - mPaddingBottom); mGroupFlags &= ~CLIP_TO_PADDING_MASK; } final boolean drawSelectorOnTop = mDrawSelectorOnTop; if (!drawSelectorOnTop) { //将Selector画为背景 drawSelector(canvas); } super.dispatchDraw(canvas);// 用Canvas画ListView if (drawSelectorOnTop) { //将Selector画为前景 drawSelector(canvas); } if (clipToPadding) { canvas.restoreToCount(saveCount); mGroupFlags |= CLIP_TO_PADDING_MASK; } } private void drawSelector(Canvas canvas) { if (!mSelectorRect.isEmpty()) { final Drawable selector = mSelector; selector.setBounds(mSelectorRect);//设置drawable画的区域 selector.draw(canvas); //使用canvas将drawable画上去 } }
看到这里,想必大家都已经明白如何画前景和背景了吧。在dispatchDraw之前调用就是画前景,在dispatchDraw之后调用就是画背景。
另外补充一下,本文并没有介绍动画部分,有兴趣的可以自己研究下。
总结一下,实现这个功能需要有三个步骤:
1.设置Selector,并更新状态(初始化时候)
2.确定Selector画的区域,设置View的状态,根据View状态,更新Selector的状态(一般是对Event的处理方法中)
3.使用Canvas在dispatchDraw中,将Selector画上去,画Drawable的时候需要先设置区域,再调用drawable的draw方法。
后面我再将View如何画背景和前景补上,今天就先到这里吧。