自定义View(二-番外9)

来源:互联网 发布:游戏抽奖软件 编辑:程序博客网 时间:2024/06/11 21:51

From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige

跟着爱哥打天下

自定义控件其实很简单9:

即便你是第一天学习Android,你也一定会用到Activity,用到Activity你一定会接触到onCreate方法,然后你会从各种途径了解到类似这样的方法还有7个,我们称之为Activity生命周期:

/**  * 主界面  *   * @author Aige {@link http://blog.csdn.net/aigestudio}  * @since 2014/11/17  */  public class MainActivity extends Activity {      @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);      }      @Override      protected void onStart() {          super.onStart();      }      @Override      protected void onResume() {          super.onResume();      }      @Override      protected void onPause() {          super.onPause();      }      @Override      protected void onStop() {          super.onStop();      }      @Override      protected void onRestart() {          super.onRestart();      }      @Override      protected void onDestroy() {          super.onDestroy();      }  }  

Android framework在Activity的不同时期调用不同的生命周期方法并将其提供给我们以便我们能在Activity加载的不同时期根据自己的需要做不同的事,For example:我们可以在onCreate中设置我们的布局文件或显示的View,在onStop中处理Activity位于后台时的操作,在onDestroy中销毁一些不必要的强引用避免在Activity销毁后造成泄漏等等等等,这些方法给我们控制Activity带来了极大的便利,而在View中我们也学习了onMeasure、onLayout和onDraw这三个类似的方法,它们也是依次在View创建的不同时期被调用,那么是否还应存在其他类似的方法呢?The answer is yes,View也提供了一下几个类似“生命周期”的方法:

这里写图片描述

如上所示的这些方法,除了提到的“生命周期”方法外还有一些事件的回调,多说无益,我们还是来看看这些方法会在View的什么时候被调用,老样子我们新建一个继承于View的子类并重写这些方法:

/**  *   * @author AigeStudio {@link http://blog.csdn.net/aigestudio}  * @since 2015/1/27  *   */  public class LifeCycleView extends View {      private static final String TAG = "AigeStudio:LifeCycleView";      public LifeCycleView(Context context) {          super(context);          Log.d(TAG, "Construction with single parameter");      }      public LifeCycleView(Context context, AttributeSet attrs) {          super(context, attrs);          Log.d(TAG, "Construction with two parameters");      }      @Override      protected void onFinishInflate() {          super.onFinishInflate();          Log.d(TAG, "onFinishInflate");      }      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          super.onMeasure(widthMeasureSpec, heightMeasureSpec);          Log.d(TAG, "onMeasure");      }      @Override      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {          super.onLayout(changed, left, top, right, bottom);          Log.d(TAG, "onLayout");      }      @Override      protected void onSizeChanged(int w, int h, int oldw, int oldh) {          super.onSizeChanged(w, h, oldw, oldh);          Log.d(TAG, "onSizeChanged");      }      @Override      protected void onDraw(Canvas canvas) {          super.onDraw(canvas);          Log.d(TAG, "onDraw");      }      @Override      protected void onAttachedToWindow() {          super.onAttachedToWindow();          Log.d(TAG, "onAttachedToWindow");      }      @Override      protected void onDetachedFromWindow() {          super.onDetachedFromWindow();          Log.d(TAG, "onDetachedFromWindow");      }      @Override      protected void onWindowVisibilityChanged(int visibility) {          super.onWindowVisibilityChanged(visibility);          Log.d(TAG, "onWindowVisibilityChanged");      }  }  

上面的方法中我过滤掉了事件和焦点触发的方法,仅看View运行时被调用的方法,运行看看我们LogCat中的输出:

这里写图片描述

首先是调用了构造方法,这是不用猜都该知道的,然后呢调用了onFinishInflate方法,这个方法当xml布局中我们的View被解析完成后则会调用,具体的实现在LayoutInflater的rInflate方法中:

public abstract class LayoutInflater {      void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,              boolean finishInflate) throws XmlPullParserException, IOException {          // 省去无数代码…………          if (finishInflate) parent.onFinishInflate();      }  }  

也就是说如果我们不从xml布局文件中解析的话,该方法就不会被调用,我们在Activity直接加载View的实例:

public class MainActivity extends Activity {      @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(new LifeCycleView(this));      }  }  

这时再次运行我们的APP,就会发现不会再去调用onFinishInflate方法:

这里写图片描述

紧接着调用的是onAttachedToWindow方法,此时表示我们的View已被创建并添加到了窗口Window中,该方法后紧接着一般会调用onWindowVisibilityChanged方法,只要我们当前的Window窗口中View的可见状态发生改变都会被触发,这时View是被显示了,随后就会开始调用onMeasure方法对View进行测量,如果测量结果被确定则会先调用onSizeChanged方法通知View尺寸大小发生了改变,紧跟着便会调用onLayout方法对子元素进行定位布局,然后再次调用onMeasure方法对View进行二次测量,如果测量值与上一次相同则不再调用onSizeChanged方法,接着再次调用onLayout方法,如果测量过程结束,则会调用onDraw方法绘制View。我们看到,onMeasure和onLayout方法被调用了两次,很多童鞋会很纠结为何onMeasure方法回被多次调用,其实没必要过于纠结这个问题,onMeasure的调用取决于控件的父容器以及View Tree的结构,不同的父容器有不同的测量逻辑,比如上一节自定义控件其实很简单2/3中,我们在SquareLayout测量子元素时就采取了二次测量,在API 19的时候Android对测量逻辑做了进一步的优化,比如在19之前只会对最后一次的测量结果进行Cache而在19开始则会对每一次测量结果都进行Cache,如果相同的代码相同布局相同的逻辑在19和19之前你有可能会看到不一样的测量次数结果,所以没必要去纠结这个问题,一般情况下只要你逻辑正确onMeasure都会得到正确的调用。
上面这些方法都很好理解,我们主要关心的是其调用流程,虽然上面我们通过LogCat的输出大致了解了一下其执行顺序,但是如果你好奇心足够重,一定会想真是这样的么?在自定义控件其实很简单7/12中我曾留下一个疑问:

/**  *   * @author AigeStudio {@link http://blog.csdn.net/aigestudio}  * @since 2015/1/12  *   */  public class ImgView extends View {      private Bitmap mBitmap;// 位图对象      public ImgView(Context context, AttributeSet attrs) {          super(context, attrs);      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制位图          canvas.drawBitmap(mBitmap, 0, 0, null);      }      /**      * 设置位图      *       * @param bitmap      *            位图对象      */      public void setBitmap(Bitmap bitmap) {          this.mBitmap = bitmap;      }  }  

就是如上代码片段是否有什么问题?细心的盆友其实已经发现了,我们在onDraw中用到的mBitmap竟不为null,按照我们上面分析的结果,如果顺次调用View的各个方法,那么此时如果我们在Activity中调用setBitmap方法为我们的ImgView设置Bitmap:

public class MainActivity extends Activity {      private ImgView mImgView;      @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          mImgView = (ImgView) findViewById(R.id.main_pv);          Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);          mImgView.setBitmap(bitmap);      }  }  

的话onDraw方法获取到的Bitmap应该为null才对,或者直白地说setBitmap方法应该在onDraw之后才被调用才对。对么?如果你这样想,别说Android,甚至连Java基础都不过关~~~为什么这么说呢?记住上面这些除了构造方法外的onXXX方法,都是一系列的回调方法,当且仅当一定条件成立才会被触发,比如上面说过的onFinishInflate方法,只有在该View以及其子View从xml解析完毕才会被调用,打个比方,如果我们编译Android源代码,尝试在rInflate方法解析完xml生前View调用onFinishInflate之前调用我们的setBitmap方法,这时候你就会看到执行顺序的改变。具体的原理设计太多的framework源码,在该系列我就不在多贴一些系统的源码了,如果你想更深地了解,我会在后续的《深入剖析Android GUI框架》系列中详细阐述,这里我仅作简单的介绍,注意到上面我在说View的“生命周期”时使用了一个引号,虽然说到上面的一些方法会在View运行过程中依次被调用,但事实上真正称得上View的生命周期的阶段只有三个:
处理控件动画的阶段
处理测量的阶段
处理绘制的阶段
Android的Animation动画体系庞大不在本系列的讲解范畴内,暂时Skip,测量和绘制的主要过程由我们之前所讲的三个方法onMeasure、onLayout和onDraw所控制,这三个方法呢在framework中又主要由measure、layout、draw以及其派生方法所控制,在View中形成这样一个体系:

这里写图片描述

再次注意:View的测量过程是由多个方法调用共同构成,measure和onMeasure仅仅代表该过程中的两个方法而已。
如果控件继承于ViewGroup实现的是一个布局容器,那么会多出一个dispatchDraw方法:

这里写图片描述

dispatchDraw方法本质上实现的是父容器对子元素的绘制分发,虽然逻辑不尽相同但是作用类似于draw,在高仿网易评论列表效果之界面生成中我们曾利用该方法在绘制子元素前绘制盖楼背景,具体不再多说了。在我们调用setContentView方法后,如果你传入的是一个资源文件ID,此时framework会使用LayoutInflater去解析布局文件,当解析到我们自定义控件LifeCycleView的标签时,通过反射获取一个对应的LifeCycleView类实例,此时构造方法被调用,尔后开始解析LifeCycleView标签下的各类属性并存值,LifeCycleView标签下的所有属性(如果是个容器的话也会层层解析)解析完成后调用onFinishInflate方法表示当前LifeCycleView所有的(注意不是整个布局哦仅仅是该View对应标签)xml解析完毕,之后尝试将View添加至当前Activity所在的Window,然后将处理UI事件的Msg压入Message Queue开始至上而下地对整个View Tree进行测量,假设我们有如下的View Tree结构:

这里写图片描述

那么我们的测量总是从根部RelativeLayout开始逐层往下进行调用,在Android翻页效果原理实现之引入折线中我们曾在讲滑动时对Message Queue作过一个简单的浅析,当Msg压入Queue并最终得到处理的这段过程并不是立即的,也就是说其中会有一定的延时,这相对于我们在setContentView后立即setBitmap来说时间要长很多很多,这也是为什么我们在onMeasure中获取Bitmap不为null的原因,具体的源码逻辑实现会在《深入剖析Android GUI框架》深度讲解,本系列除了后面要涉及到的事件分发外不会再涉及过多的源码毕竟与基础篇的定位不符,好了,这里我再留一个问题,setBitmap和onMeasure、onLayout等这些回调方法之间是异步呢还是同步呢?其实答案很明显了……OK,不说了,既然我们知道这样直接setBitmap是不对的(即便可行)那么我们该如何改进呢?答案很简单,Andorid提供给我们极其简便的方法,我们只需在设置Bitmap后调用requestLayout方法和invalidate即可:

public void setBitmap(Bitmap bitmap) {      this.mBitmap = bitmap;      requestLayout();      invalidate();  }  

requestLayout方法的意义在于如果你的操作有可能会让控件的尺寸或位置发生改变那么就可以调用该方法请求布局,调用该方法后framework会尝试调用measure对控件重新测量:

这里写图片描述

而invalidate方法呢我们则用的多了不再多说:
但是要注意的一点是,requestLayout方法和invalidate方法并非都必需调用的,比如我们有一个更改字体颜色的方法:

public void setTextColor(int color) {      mPaint.setColor(color);      invalidate();  }  

这时我们仅需调用invalidate方法标识重绘视图即可,但是,如上我们所说,如果一旦尺寸大小或位置发生了变化,那么我们最好重新布局并迫使视图重绘,比如我们有个改变字体大小的方法:

public void setTextSize(int size) {      mPaint.setTextSize(size);      requestLayout();      invalidate();  }  

这时候我们就需要调用requestLayout请求布局,因为字体大小的改变有可能会影响到控件的尺寸大小和位置的改变,同样,如果位置大小都变了,那我们是否该重新绘制呢?The answer is yes~好了,别嫌我啰嗦,最近有盆友反应说前面章节太难理解……其实之所以觉得前面的章节难是因为涉及绘制的API大多跟一些图像处理有关,而coder正恰恰缺乏这方面的一些知识所以不好理解,你看我前面的章节压根就没涉及什么源码,只有从测量开始才涉及了一次,此后也不再打算再过多地涉及,毕竟与该系列基础篇的定位不符。闲话不多说了,如上以及前面两节的内容所述,其实在应用开发使用的过程中设计测量逻辑的API并不多,也没太多可讲的,最主要的还是自己的逻辑


原创粉丝点击