1、View的几种不同状态属性
android背景选择器selector用法汇总
对Android开发者来说,<selector>节点的使用一定很熟悉,该节点的作用就是定义一组状态资源图片,使其能够
在不同的状态下更换某个View的背景图片。例如hello_selection.xml文件定义:
<span style="white-space:pre"> </span><?xml version="1.0" encoding="utf-8" ?> <span style="white-space:pre"> </span><selector xmlns:android="http://schemas.android.com/apk/res/android"> <span style="white-space:pre"> </span><!-- 触摸时并且当前窗口处于交互状态 --> <span style="white-space:pre"> </span><item android:state_pressed="true" android:state_window_focused="true" android:drawable= "@drawable/pic1" /> <span style="white-space:pre"> </span><!-- 触摸时并且没有获得焦点状态 --> <span style="white-space:pre"> </span><item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/pic2" /> <span style="white-space:pre"> </span><!--选中时的图片背景--> <span style="white-space:pre"> </span><item android:state_selected="true" android:drawable="@drawable/pic3" /> <span style="white-space:pre"> </span><!--获得焦点时的图片背景--> <span style="white-space:pre"> </span><item android:state_focused="true" android:drawable="@drawable/pic4" /> <span style="white-space:pre"> </span><!-- 窗口没有处于交互时的背景图片 --> <span style="white-space:pre"> </span><item android:drawable="@drawable/pic5" /> <span style="white-space:pre"> </span></selector>
其实,这里的Seletor的xml文件,最终会被Android框架解析成StateListDrawable类对象。
知识点一:StateListDrawable类介绍
类功能说明:该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。
常用方法为:
public void addState (int[] stateSet, Drawable drawable)
功能: 给特定的状态集合设置相应drawable图片资源
使用方式:参考前面的hello_selection.xml文件,可以使用代码构建一个相同的StateListDrawable类对象,如下:
<span style="white-space:pre"> </span>//初始化一个空对象 <span style="white-space:pre"> </span>StateListDrawable stalistDrawable = new StateListDrawable(); <span style="white-space:pre"> </span>//获取对应的属性值 Android框架自带的属性 attr <span style="white-space:pre"> </span>int pressed = android.R.attr.state_pressed; <span style="white-space:pre"> </span>int window_focused = android.R.attr.state_window_focused; <span style="white-space:pre"> </span>int focused = android.R.attr.state_focused; <span style="white-space:pre"> </span>int selected = android.R.attr.state_selected; <span style="white-space:pre"> </span>stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1)); <span style="white-space:pre"> </span>stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2); <span style="white-space:pre"> </span>stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3); <span style="white-space:pre"> </span>stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4); <span style="white-space:pre"> </span>//没有任何状态时显示的图片,我们给它设置我空集合 <span style="white-space:pre"> </span>stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5);
上面的“—”负号表示对应的属性值为false, 当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。
public boolean isStateful ()
功能: 表明该状态改变了,对应的drawable图片是否会改变。
注:在StateListDrawable类中,该方法返回为true,显然状态改变后,我们的图片会跟着改变。
知识点二:View的五种状态值
一般来说,Android框架为View定义了四种不同的状态,这些状态值的改变会引发View相关操作,例如:更换背景图片、是否
触发点击事件等;视
视图几种不同状态含义见下图:
enable :View是否可以响应事件,比如点击,触摸等。
focused:是否有焦点状态。
pressed:是否处于按下状态。
selected:是否处于选中状态。
window_focused:视图所在窗口是否是处在交互窗口的状态。
2、一个窗口只能有一个视图获得焦点(focus),而一个窗口可以有多个视图处于”selected”状态中。
总结:focused状态一般是由按键操作引起的;
pressed状态是由触摸消息引起的;
selected则完全是由应用程序主动调用setSelected()进行控制。
例如:当我们触摸某个控件时,会导致pressed状态改变;获得焦点时,会导致focus状态变化。于是,我们可以通过这种
更新后状态值去更新我们对应的Drawable对象了。
2、如何根据不同状态去切换我们的背景图片。
当View任何状态值发生改变时,都会调用refreshDrawableList()方法去更新对应的背景Drawable对象。
其整体调用流程如下: View.java类中(//路径:\frameworks\base\core\java\android\view\View.java)
/* Call this to force a view to update its drawable state. This will cause * drawableStateChanged to be called on this view. Views that are interested * in the new state should call getDrawableState. */ //主要功能是根据当前的状态值去更换对应的背景Drawable对象 public void refreshDrawableState() { mPrivateFlags |= DRAWABLE_STATE_DIRTY; //所有功能在这个函数里去完成 drawableStateChanged(); ... }
/* This function is called whenever the state of the view changes in such * a way that it impacts the state of drawables being shown. */ // 获得当前的状态属性--- 整型集合 ; 调用Drawable类的setState方法去获取资源。 protected void drawableStateChanged() { //该视图对应的Drawable对象,通常对应于StateListDrawable类对象 Drawable d = mBGDrawable; if (d != null && d.isStateful()) { //通常都是成立的 //getDrawableState()方法主要功能:会根据当前View的状态属性值,将其转换为一个整型集合 //setState()方法主要功能:根据当前的获取到的状态,更新对应状态下的Drawable对象。 d.setState(getDrawableState()); } }
/*Return an array of resource IDs of the drawable states representing the * current state of the view. */ public final int[] getDrawableState() { if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) { return mDrawableState; } else { //根据当前View的状态属性值,将其转换为一个整型集合,并返回 mDrawableState = onCreateDrawableState(0); mPrivateFlags &= ~DRAWABLE_STATE_DIRTY; return mDrawableState; } }
通过这段代码我们可以明白View内部是如何获取更细后的状态值以及动态获取对应的背景Drawable对象----setState()方法
去完成的。这儿我简单的分析下Drawable类里的setState()方法的功能,把流程给走一下:
Step 1 、 setState()函数原型 ,
函数位于:frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
//如果状态态值发生了改变,就回调onStateChange()方法。 public boolean setState(final int[] stateSet) { if (!Arrays.equals(mStateSet, stateSet)) { mStateSet = stateSet; return onStateChange(stateSet); } return false; }
该函数的主要功能: 判断状态值是否发生了变化,如果发生了变化,就调用onStateChange()方法进一步处理。
Step 2 、onStateChange()函数原型:
该函数位于 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
//状态值发生了改变,我们需要找出第一个吻合的当前状态的Drawable对象 protected boolean onStateChange(int[] stateSet) { //要找出第一个吻合的当前状态的Drawable对象所在的索引位置, 具体匹配算法请自己深入源码看看 int idx = mStateListState.indexOfStateSet(stateSet); ... //获取对应索引位置的Drawable对象 if (selectDrawable(idx)) { return true; } ... }
该函数的主要功能: 根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处 ;
继而,调用selectDrawable()方法去获取索引下标的当前Drawable对象。
具体查找算法在 mStateListState.indexOfStateSet(stateSet) 里实现了。基本思路是:查找第一个能完全吻合该新状态值
的索引下标,如果找到了,则立即返回。 具体实现过程,只好看看源码咯。
Step 3 、selectDrawable()函数原型:
该函数位于 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
public boolean selectDrawable(int idx) { if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) { //获取对应索引位置的Drawable对象 Drawable d = mDrawableContainerState.mDrawables[idx]; ... mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象 mCurIndex = idx; ... } else { ... } //请求该View刷新自己,这个方法我们稍后讲解。 invalidateSelf(); return true; }
知识点三: 关于Drawable.Callback接口
该接口定义了如下三个函数:
//该函数位于 frameworks\base\graphics\java\android\graphics\drawable\Drawable.java 类中 public static interface Callback { //如果Drawable对象的状态发生了变化,会请求View重新绘制, //因此我们对应于该View的背景Drawable对象能够”绘制出来”. public void invalidateDrawable(Drawable who); // public void scheduleDrawable(Drawable who, Runnable what, long when); // public void unscheduleDrawable(Drawable who, Runnable what); }
其中比较重要的函数为:
public voidinvalidateDrawable(Drawable who)
函数功能:如果Drawable对象的状态发生了变化,会请求View重新绘制,因此我们对应于该View的背景Drawable对象
能够重新”绘制“出来。
Android框架View类继承了该接口,同时实现了这三个函数的默认处理方式,其中invalidateDrawable()方法如下:
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... //Invalidates the specified Drawable. //默认实现,重新绘制该视图本身 public void invalidateDrawable(Drawable drawable) { if (verifyDrawable(drawable)) { //是否是同一个Drawable对象,通常为真 final Rect dirty = drawable.getBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; //重新请求绘制该View,即重新调用该View的draw()方法 ... invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); } } ... }
因此,我们的Drawable类对象必须将View设置为回调对象,否则,即使改变了状态,也不会显示对应的背景图。 如下:
Drawable d ; // 图片资源
d.setCallback(View v) ; // 视图v的背景资源为 d 对象
知识点四:View绘制背景图片过程
在前面的博客中《Android中View绘制流程以及invalidate()等相关方法分析》,我们知道了一个视图的背景绘制过程时在
View类里的draw()方法里完成的,我们这儿在回顾下draw()的流程,同时重点讲解下绘制背景的操作。
//方法所在路径:frameworks\base\core\java\android\view\View.java //draw()绘制过程 private void draw(Canvas canvas){ //该方法会做如下事情 //1 、绘制该View的背景 //其中背景图片绘制过程如下: //是否透明, 视图通常是透明的 , 为true if (!dirtyOpaque) { //开始绘制视图的背景 final Drawable background = mBGDrawable; if (background != null) { final int scrollX = mScrollX; //获取偏移值 final int scrollY = mScrollY; //视图的布局坐标是否发生了改变, 即是否重新layout了。 if (mBackgroundSizeChanged) { //如果是,我们的Drawable对象需要重新设置大小了,即填充该View。 background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; } //View没有发生偏移 if ((scrollX | scrollY) == 0) { background.draw(canvas); //OK, 该方法会绘制当前StateListDrawable的当前背景Drawable } else { //View发生偏移,由于背景图片值显示在布局坐标中,即背景图片不会发生偏移,只有视图内容onDraw()会发生偏移 //我们调整canvas对象的绘制区域,绘制完成后对canvas对象属性调整回来 canvas.translate(scrollX, scrollY); background.draw(canvas); //OK, 该方法会绘制当前StateListDrawable的当前背景Drawable canvas.translate(-scrollX, -scrollY); } } } ... //2、为绘制渐变框做一些准备操作 //3、调用onDraw()方法绘制视图本身 //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。 //5、绘制渐变框 }
That's all ! 我们用到的知识点也就这么多吧。 如果大家有丝丝不明白的话,可以去看下源代码,具体去分析下这些流程到底
是怎么走下来的。
我们从宏观的角度分析了View绘制不同状态背景的原理,View框架就是这么做的。为了易于理解性,
下面我们通过一个小Demo来演示前面种种流程。
Demo 说明:
我们参照View框架中绘制不同背景图的实现原理,自定义一个View类,通过给它设定StateListDrawable对象,使其能够在
不同状态时能动态"绘制"背景图片。 基本流程方法和View.java类实现过程一模一样。
截图如下:
初始背景图 触摸后显示的背景图(pressed)
一、主文件MainActivity.java如下:
/** * * @author http://http://blog.csdn.net/qinjuning */ public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout ll = new LinearLayout(MainActivity.this); CustomView customView = new CustomView(MainActivity.this); //简单设置为 width 200px - height 100px吧 ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(200 , 100); customView.setLayoutParams(lp); //需要将该View设置为可点击/触摸状态,否则触摸该View没有效果。 customView.setClickable(true); ll.addView(customView); setContentView(ll); } }
功能很简单,为Activity设置了视图 。
二、 自定义View如下 , CustomView.java :
/** * @author http://http://blog.csdn.net/qinjuning */ //自定义View public class CustomView extends View /*extends Button*/ { private static String TAG = "TackTextView"; private Context mContext = null; private Drawable mBackground = null; private boolean mBGSizeChanged = true;; //视图View布局(layout)大小是否发生变化 public CustomView(Context context) { super(context); mContext = context; initStateListDrawable(); // 初始化图片资源 } // 初始化图片资源 private void initStateListDrawable() { //有两种方式获取我们的StateListDrawable对象: // 获取方式一、手动构建一个StateListDrawable对象 StateListDrawable statelistDrawable = new StateListDrawable(); int pressed = android.R.attr.state_pressed; int windowfocused = android.R.attr.state_window_focused; int enabled = android.R.attr.state_enabled; int stateFoucesd = android.R.attr.state_focused; //匹配状态时,是一种优先包含的关系。 // "-"号表示该状态值为false .即不匹配 statelistDrawable.addState(new int[] { pressed, windowfocused }, mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed)); statelistDrawable.addState(new int[]{ -pressed, windowfocused }, mContext.getResources().getDrawable(R.drawable.btn_power_on_nor)); mBackground = statelistDrawable; //必须设置回调,当改变状态时,会回掉该View进行invalidate()刷新操作. mBackground.setCallback(this); //取消默认的背景图片,因为我们设置了自己的背景图片了,否则可能造成背景图片重叠。 this.setBackgroundDrawable(null); // 获取方式二、、使用XML获取StateListDrawable对象 // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background); } protected void drawableStateChanged() { Log.i(TAG, "drawableStateChanged"); Drawable d = mBackground; if (d != null && d.isStateful()) { d.setState(getDrawableState()); Log.i(TAG, "drawableStateChanged and is 111"); } Log.i(TAG, "drawableStateChanged and is 222"); super.drawableStateChanged(); } //验证图片是否相等 , 在invalidateDrawable()会调用此方法,我们需要重写该方法。 protected boolean verifyDrawable(Drawable who) { return who == mBackground || super.verifyDrawable(who); } //draw()过程,绘制背景图片... public void draw(Canvas canvas) { Log.i(TAG, " draw -----"); if (mBackground != null) { if(mBGSizeChanged) { //设置边界范围 mBackground.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); mBGSizeChanged = false ; } if ((getScrollX() | getScrollY()) == 0) //是否偏移 { mBackground.draw(canvas); //绘制当前状态对应的图片 } else { canvas.translate(getScrollX(), getScrollY()); mBackground.draw(canvas); //绘制当前状态对应的图片 canvas.translate(-getScrollX(), -getScrollY()); } } super.draw(canvas); } public void onDraw(Canvas canvas) { ... } }
将该View设置的背景图片转换为节点xml,形式如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:state_window_focused="true" android:drawable="@drawable/btn_power_on_pressed"></item> <item android:state_pressed="false" android:state_window_focused="true" android:drawable="@drawable/btn_power_on_nor"></item> </selector>
基本上所有功能都在这儿显示出来了, 和我们前面说的一模一样吧。
当然了,如果你想偷懒,大可用系统定义好的一套工具 , 即直接使用setBackgroundXXX()或者在设置对应的属性,但是,
万变不离其宗,掌握了绘制原理,可以潇洒走江湖了。
示例Demo下载地址: http://download.csdn.net/detail/qinjuning/4237298