Android自定义View的实现方法,带你一步步深入了解View(三) 。
在前面一篇文章中,我带着大家一起从源码的层面上分析了视图的绘制流程,了解了视图绘制流程中onMeasure、onLayout、onDraw这三个最重要步骤的工作原理,那么今天我们将继续对View进行深入探究,学习一下视图状态以及重绘方面的知识。如果你还没有看过我前面一篇文章,可以先去阅读Android视图绘制流程完全解析,带你一步步深入了解View(二)。
相信大家在平时使用View的时候都会发现它是有状态的,比如说有一个按钮,普通状态下是一种效果,但是当手指按下的时候就会变成另外一种效果,这样才会给人产生一种点击了按钮的感觉。当然了,这种效果相信几乎所有的Android程序员都知道该如何实现,但是我们既然是深入了解View,那么自然也应该知道它背后的实现原理应该是什么样的,今天就让我们来一起探究一下吧。
一、视图状态
视图状态的种类非常多,一共有十几种类型,不过多数情况下我们只会使用到其中的几种,因此这里我们也就只去分析最常用的几种视图状态。
1. enabled
表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。
2. focused
表示当前视图是否获得到焦点。通常情况下有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法。而现在的Android手机几乎都没有键盘了,因此基本上只可以使用requestFocus()这个办法来让视图获得焦点了。而requestFocus()方法也不能保证一定可以让视图获得焦点,它会有一个布尔值的返回值,如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText。
3. window_focused
表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。
4. selected
表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中。
5. pressed
表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,但开发者也可以自己调用这个方法来进行改变。
我们可以在项目的drawable目录下创建一个selector文件,在这里配置每种状态下视图对应的背景图片。比如创建一个compose_bg.xml文件,在里面编写如下代码:
[html] view plain copy- <selectorxmlns:android="http://schemas.android.com/apk/res/android">
- <itemandroid:drawable="@drawable/compose_pressed"android:state_pressed="true"></item>
- <itemandroid:drawable="@drawable/compose_pressed"android:state_focused="true"></item>
- <itemandroid:drawable="@drawable/compose_normal"></item>
- </selector>
创建好了这个selector文件后,我们就可以在布局或代码中使用它了,比如将它设置为某个按钮的背景图,如下所示:
[html] view plain copy- <?xmlversion="1.0"encoding="utf-8"?>
- <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
- <Button
- android:id="@+id/compose"
- android:layout_width="60dp"
- android:layout_height="40dp"
- android:layout_gravity="center_horizontal"
- android:background="@drawable/compose_bg"
- />
- </LinearLayout>
这样我们就用一个非常简单的方法实现了按钮按下的效果,但是它的背景原理到底是怎样的呢?这就又要从源码的层次上进行分析了。
我们都知道,当手指按在视图上的时候,视图的状态就已经发生了变化,此时视图的pressed状态是true。每当视图的状态有发生改变的时候,就会回调View的drawableStateChanged()方法,代码如下所示:
[java] view plain copy- protectedvoiddrawableStateChanged(){
- Drawabled=mBGDrawable;
- if(d!=null&&d.isStateful()){
- d.setState(getDrawableState());
- }
- }
- publicvoidsetBackgroundResource(intresid){
- if(resid!=0&&resid==mBackgroundResource){
- return;
- }
- Drawabled=null;
- if(resid!=0){
- d=mResources.getDrawable(resid);
- }
- setBackgroundDrawable(d);
- mBackgroundResource=resid;
- }
而我们在布局文件中通过android:background属性指定的selector文件,效果等同于调用setBackgroundResource()方法。也就是说drawableStateChanged()方法中的mBGDrawable对象其实就是我们指定的selector文件。
接下来在drawableStateChanged()方法的第4行调用了getDrawableState()方法来获取视图状态,代码如下所示:
[java] view plain copy- publicfinalint[]getDrawableState(){
- if((mDrawableState!=null)&&((mPrivateFlags&DRAWABLE_STATE_DIRTY)==0)){
- returnmDrawableState;
- }else{
- mDrawableState=onCreateDrawableState(0);
- mPrivateFlags&=~DRAWABLE_STATE_DIRTY;
- returnmDrawableState;
- }
- }
在得到了视图状态的数组之后,就会调用Drawable的setState()方法来对状态进行更新,代码如下所示:
[java] view plain copy- publicbooleansetState(finalint[]stateSet){
- if(!Arrays.equals(mStateSet,stateSet)){
- mStateSet=stateSet;
- returnonStateChange(stateSet);
- }
- returnfalse;
- }
- @Override
- protectedbooleanonStateChange(int[]stateSet){
- intidx=mStateListState.indexOfStateSet(stateSet);
- if(DEBUG)android.util.Log.i(TAG,"onStateChange"+this+"states"
- +Arrays.toString(stateSet)+"found"+idx);
- if(idx<0){
- idx=mStateListState.indexOfStateSet(StateSet.WILD_CARD);
- }
- if(selectDrawable(idx)){
- returntrue;
- }
- returnsuper.onStateChange(stateSet);
- }
可以看到,这里会先调用indexOfStateSet()方法来找到当前视图状态所对应的Drawable资源下标,然后在第9行调用selectDrawable()方法并将下标传入,在这个方法中就会将视图的背景图设置为当前视图状态所对应的那张图片了。
那你可能会有疑问,在前面一篇文章中我们说到,任何一个视图的显示都要经过非常科学的绘制流程的,很显然,背景图的绘制是在draw()方法中完成的,那么为什么selectDrawable()方法能够控制背景图的改变呢?这就要研究一下视图重绘的流程了。
二、视图重绘
虽然视图会在Activity加载完成之后自动绘制到屏幕上,但是我们完全有理由在与Activity进行交互的时候要求动态更新视图,比如改变视图的状态、以及显示或隐藏某个控件等。那在这个时候,之前绘制出的视图其实就已经过期了,此时我们就应该对视图进行重绘。
调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的,那么就让我们来看一看invalidate()方法的代码是什么样的吧。
View的源码中会有数个invalidate()方法的重载和一个invalidateDrawable()方法,当然它们的原理都是相同的,因此我们只分析其中一种,代码如下所示:
[java] view plain copy- voidinvalidate(booleaninvalidateCache){
- if(ViewDebug.TRACE_HIERARCHY){
- ViewDebug.trace(this,ViewDebug.HierarchyTraceType.INVALIDATE);
- }
- if(skipInvalidate()){
- return;
- }
- if((mPrivateFlags&(DRAWN|HAS_BOUNDS))==(DRAWN|HAS_BOUNDS)||
- (invalidateCache&&(mPrivateFlags&DRAWING_CACHE_VALID)==DRAWING_CACHE_VALID)||
- (mPrivateFlags&INVALIDATED)!=INVALIDATED||isOpaque()!=mLastIsOpaque){
- mLastIsOpaque=isOpaque();
- mPrivateFlags&=~DRAWN;
- mPrivateFlags|=DIRTY;
- if(invalidateCache){
- mPrivateFlags|=INVALIDATED;
- mPrivateFlags&=~DRAWING_CACHE_VALID;
- }
- finalAttachInfoai=mAttachInfo;
- finalViewParentp=mParent;
- if(!HardwareRenderer.RENDER_DIRTY_REGIONS){
- if(p!=null&&ai!=null&&ai.mHardwareAccelerated){
- p.invalidateChild(this,null);
- return;
- }
- }
- if(p!=null&&ai!=null){
- finalRectr=ai.mTmpInvalRect;
- r.set(0,0,mRight-mLeft,mBottom-mTop);
- p.invalidateChild(this,r);
- }
- }
- }
- publicfinalvoidinvalidateChild(Viewchild,finalRectdirty){
- ViewParentparent=this;
- finalAttachInfoattachInfo=mAttachInfo;
- if(attachInfo!=null){
- finalbooleandrawAnimation=(child.mPrivateFlags&DRAW_ANIMATION)==DRAW_ANIMATION;
- if(dirty==null){
- ......
- }else{
- ......
- do{
- Viewview=null;
- if(parentinstanceofView){
- view=(View)parent;
- if(view.mLayerType!=LAYER_TYPE_NONE&&
- view.getParent()instanceofView){
- finalViewgrandParent=(View)view.getParent();
- grandParent.mPrivateFlags|=INVALIDATED;
- grandParent.mPrivateFlags&=~DRAWING_CACHE_VALID;
- }
- }
- if(drawAnimation){
- if(view!=null){
- view.mPrivateFlags|=DRAW_ANIMATION;
- }elseif(parentinstanceofViewRootImpl){
- ((ViewRootImpl)parent).mIsAnimating=true;
- }
- }
- if(view!=null){
- if((view.mViewFlags&FADING_EDGE_MASK)!=0&&
- view.getSolidColor()==0){
- opaqueFlag=DIRTY;
- }
- if((view.mPrivateFlags&DIRTY_MASK)!=DIRTY){
- view.mPrivateFlags=(view.mPrivateFlags&~DIRTY_MASK)|opaqueFlag;
- }
- }
- parent=parent.invalidateChildInParent(location,dirty);
- if(view!=null){
- Matrixm=view.getMatrix();
- if(!m.isIdentity()){
- RectFboundingRect=attachInfo.mTmpTransformRect;
- boundingRect.set(dirty);
- m.mapRect(boundingRect);
- dirty.set((int)boundingRect.left,(int)boundingRect.top,
- (int)(boundingRect.right+0.5f),
- (int)(boundingRect.bottom+0.5f));
- }
- }
- }while(parent!=null);
- }
- }
- }
- publicViewParentinvalidateChildInParent(finalint[]location,finalRectdirty){
- invalidateChild(null,dirty);
- returnnull;
- }
- publicvoidinvalidateChild(Viewchild,Rectdirty){
- checkThread();
- if(LOCAL_LOGV)Log.v(TAG,"Invalidatechild:"+dirty);
- mDirty.union(dirty);
- if(!mWillDrawSoon){
- scheduleTraversals();
- }
- }
- publicvoidscheduleTraversals(){
- if(!mTraversalScheduled){
- mTraversalScheduled=true;
- sendEmptyMessage(DO_TRAVERSAL);
- }
- }
- publicvoidhandleMessage(Messagemsg){
- switch(msg.what){
- caseDO_TRAVERSAL:
- if(mProfile){
- Debug.startMethodTracing("ViewRoot");
- }
- performTraversals();
- if(mProfile){
- Debug.stopMethodTracing();
- mProfile=false;
- }
- break;
- ......
- }
熟悉的代码出现了!这里在第7行调用了performTraversals()方法,这不就是我们在前面一篇文章中学到的视图绘制的入口吗?虽然经过了很多辗转的调用,但是可以确定的是,调用视图的invalidate()方法后确实会走到performTraversals()方法中,然后重新执行绘制流程。之后的流程就不需要再进行描述了吧,可以参考Android视图绘制流程完全解析,带你一步步深入了解View(二)这一篇文章。
了解了这些之后,我们再回过头来看看刚才的selectDrawable()方法中到底做了什么才能够控制背景图的改变,代码如下所示:
[java] view plain copy- publicbooleanselectDrawable(intidx){
- if(idx==mCurIndex){
- returnfalse;
- }
- finallongnow=SystemClock.uptimeMillis();
- if(mDrawableContainerState.mExitFadeDuration>0){
- if(mLastDrawable!=null){
- mLastDrawable.setVisible(false,false);
- }
- if(mCurrDrawable!=null){
- mLastDrawable=mCurrDrawable;
- mExitAnimationEnd=now+mDrawableContainerState.mExitFadeDuration;
- }else{
- mLastDrawable=null;
- mExitAnimationEnd=0;
- }
- }elseif(mCurrDrawable!=null){
- mCurrDrawable.setVisible(false,false);
- }
- if(idx>=0&&idx<mDrawableContainerState.mNumChildren){
- Drawabled=mDrawableContainerState.mDrawables[idx];
- mCurrDrawable=d;
- mCurIndex=idx;
- if(d!=null){
- if(mDrawableContainerState.mEnterFadeDuration>0){
- mEnterAnimationEnd=now+mDrawableContainerState.mEnterFadeDuration;
- }else{
- d.setAlpha(mAlpha);
- }
- d.setVisible(isVisible(),true);
- d.setDither(mDrawableContainerState.mDither);
- d.setColorFilter(mColorFilter);
- d.setState(getState());
- d.setLevel(getLevel());
- d.setBounds(getBounds());
- }
- }else{
- mCurrDrawable=null;
- mCurIndex=-1;
- }
- if(mEnterAnimationEnd!=0||mExitAnimationEnd!=0){
- if(mAnimationRunnable==null){
- mAnimationRunnable=newRunnable(){
- @Overridepublicvoidrun(){
- animate(true);
- invalidateSelf();
- }
- };
- }else{
- unscheduleSelf(mAnimationRunnable);
- }
- animate(true);
- }
- invalidateSelf();
- returntrue;
- }
- publicvoidinvalidateSelf(){
- finalCallbackcallback=getCallback();
- if(callback!=null){
- callback.invalidateDrawable(this);
- }
- }
- publicclassViewimplementsDrawable.Callback,Drawable.Callback2,KeyEvent.Callback,
- AccessibilityEventSource{
- ......
- }
另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。这个方法中的流程比invalidate()方法要简单一些,但中心思想是差不多的,这里也就不再详细进行分析了。
这样的话,我们就将视图状态以及重绘的工作原理都搞清楚了,相信大家对View的理解变得更加深刻了。感兴趣的朋友可以继续阅读Android自定义View的实现方法,带你一步步深入了解View(四)。
更多相关文章
- 【移动开发】Android中图片过大造成内存溢出,OOM(OutOfMemory)异常
- Android 超简单的录制屏幕视频及生成GIF文件的方法
- Android处理图片OOM的若干方法小结
- exp:Android Studio调试系统源码的方法 (干货分享)
- android activity 基类 通用方法
- Android源码去除状态栏
- Android 反射获取私有方法,成员变量
- Android 杀掉自己进程的方法