AndroidView绘制流程分析及自定义View、ViewGroup进阶

来源:互联网 发布:js获取时间控件的值 编辑:程序博客网 时间:2024/06/06 17:10

这里将通过一个需求的几种不同的实现来给大家分析View的自定义。

源代码下载地址
[http://download.csdn.net/detail/u011631275/9019625]

这里写图片描述

我们先来看看最后要实现的效果。这类似一个跑马灯,一行文字,“11111 22222 33333 44444 55555 66666 77777 88888 99999” 不断滚动,99999之后又是11111。

实现这个需求有很多方法,网上可能也有很多开源的demo,关键是我们要知道哪种方法是最适合的,最优秀最有艺术感的。下面我将用几种方式实现它,有的方法并不好,有很多缺点,但是为了大家能理解自定义View,我觉得是有必要讲的。

一、第一种实现 通过重写onlayout实现
缺点:当滚动速度调快时,会有些卡顿
目标:掌握onLayout的用法

    onLayout函数的作用:重新布局所有子View在当前ViewGroup中的位置。    我的思路就是外部是一个水平方向的Linearlayout,内部是9个TextView。通过重写LinearLayout的onLayout来调整内部9个TextView的位置。然后通过不断的执行onLayout来达到这种滚动的效果。    我们来看代码
  package com.pui.view;import android.content.Context;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.view.View;import android.view.animation.AnimationUtils;import android.widget.LinearLayout;public class MarqueeLinearLayout1 extends LinearLayout { // 跑马灯的状态 是否正在滚动 private boolean MARQUEE_IS_RUNNING = false; // 是否已经初始化跑马灯滚动所需的数据 private boolean IS_INIT = false; private int childCount; // private int[] childWidths; // 滚动步长 单位时间内滚动的距离 private final int step = 2; // 多长时间layout一次 单位 毫秒 private long Interval_Time = 20; //记录开始滚动的时间 private long START_RUNNING_TIME = -1; private final int INIT_DATA = 0; private int oldDistance = 0; private final int REQUEST_LAYOUT = 1; private Handler handler = new Handler() {  @Override  public void handleMessage(Message msg) {   // TODO Auto-generated method stub   super.handleMessage(msg);   switch (msg.what) {   case INIT_DATA:    initData();    requestLayout();    break;   case REQUEST_LAYOUT:    requestLayout();    break;   default:    ;   }  } }; public MarqueeLinearLayout1(Context context, AttributeSet attrs) {  super(context, attrs);  // TODO Auto-generated constructor stub } public MarqueeLinearLayout1(Context context) {  super(context);  // TODO Auto-generated constructor stub } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  // TODO Auto-generated method stub  super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /**  * onLayout中注意  *  * onLayout中再次requestLayout无效  *  */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {  // TODO Auto-generated method stub  /**   * 这里我们能不能将else这个分支去掉呢?   * 不行。我们的目的是每次onlayout都将子View向左移动一小段距离,   * 但是并不是一开始就这样处理,我们必须先按LinearLayout   * 默认的方式处理。因为刚开始时,所有的子View都   * 还没有被布局在当前视图内部,即如果一开始我们调用childView.getLeft()   * 将等于0   *   * 我们在Activity的onCreate中如果调用了某个View的getWidth,   * 而如果这个View的宽使用wrap_content属性的话,那么获取到的宽度将为0,   * 这也是一样的道理,因为当onCreate的时候,这个View还没有被被布局到父视图中   *   * 所以我们这个视图刚显示时,按一般的LinearLayout处理,等到所有的子视图已经被布局到   * 相应的位置后再开始我们的滚动,这也是我们的start函数中   * handler.sendEmptyMessageDelayed(INIT_DATA, 500);延迟500毫秒的原因了   * 500毫秒足够Linearlayout布局完成了   *   * MARQUEE_IS_RUNNING、IS_INIT 这两个值算是开关,当我们将这两个值都改成true   * 我们的视图才会开始滚动   * MARQUEE_IS_RUNNING表示我们要开始滚动了,IS_INIT表示我们已经将需要的数据初始化了   */  // 如果已经初始化且已经开始跑动  if (MARQUEE_IS_RUNNING && IS_INIT) {   //period为从开始滚动到此刻的时间   long period = AnimationUtils.currentAnimationTimeMillis()     - START_RUNNING_TIME;   /**    * Interval_Time为20毫秒,step为2  意思是我希望滚动的速度为每20毫秒跑2个像素    * 所以distance就是从开始滚动开始计时到此刻,该滚动多少距离    */   int distance = (int) (period / Interval_Time) * step;   //下面这个可以省略,主要是如果移动距离还不到2个像素 那就忽略这次  等待下一次onLayout再一起滚动   if (distance < 2) {    handler.sendEmptyMessage(REQUEST_LAYOUT);    return;   }   /**    * 关键的来了,childCount为调用start后初始化的数据,代表当前视图的子视图的数量    * 遍历子视图      * childView.getRight()为子视图的右边界在当前视图中的位置    * oldDistance 为上一次执行onLayout时 distance的值    *    * 所以两次执行onLayout之间,滚动的距离为(distance-oldDistance)    * cright就为子视图将要滚动后的右边界    * left + oldDistance - distance就为滚动之后的左边界    * 如果这个这个值小于0,就代表有一个子View已经滚动出界了,这时候就要将它移到最右边那个子    * View的右边    *    * 啥意思呢?就是我们的内容在不断向左滚动,当有一个TextView滚动到窗口外面去了,就得将这个    * View移到所有子View的最后面,这样才能保证不断循环出现。    */   for (int i = 0; i < childCount; i++) {    View childView = getChildAt(i);    int left = childView.getLeft();    int right = childView.getRight();    int cright = right + oldDistance - distance;    if (cright < 0) {     int width = (right - left);     childView.layout(left + oldDistance - distance + width       * childCount, t, cright       + width * childCount, b);    } else {     childView.layout(left + oldDistance - distance, t, cright, b);    }   }   //记录这次onLayout的distance到oldDistance   oldDistance = distance;   /**    * onLayout执行完毕,发送异步消息,通知再次执行onLayout    * 注意:这里不能直接使用requestLayout()请求重新布局,    *    * 我们看看requestLayout中的代码,注意下面这句,它会判断父视图是不是处于    * layoutRequested的状态,我们当前就在onlayout中,所以父视图的    * isLayoutRequested()必然返回true,所以不会传递到父控件去执行    *  mParent.requestLayout();    *  所以我们记住了,不要在onlayout、layout中再去requestLayout    *      *   if (!mParent.isLayoutRequested()) {    *    mParent.requestLayout();    *      }    */   handler.sendEmptyMessage(REQUEST_LAYOUT);  } else {   super.onLayout(changed, l, t, r, b);  } } // 开始滚动跑马灯 public void start() {  handler.sendEmptyMessageDelayed(INIT_DATA, 500); } public void initData() {  MARQUEE_IS_RUNNING = true;  childCount = getChildCount();  START_RUNNING_TIME = AnimationUtils.currentAnimationTimeMillis();  IS_INIT = true; } public void stop(){  MARQUEE_IS_RUNNING = false; }}

我们再来看看布局文件main_demo1.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="vertical" ><com.pui.view.MarqueeLinearLayout1     android:id="@+id/marqueeLayout1"    android:layout_width="wrap_content"    android:layout_height="50dp"    android:orientation="horizontal"    >    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="11111"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="22222"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="33333"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="44444"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="55555"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="66666"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="77777"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="88888"/>    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        android:paddingRight="20dp"        android:text="99999"/></com.pui.view.MarqueeLinearLayout1></LinearLayout>

主Activity

package com.example.marqueedemo;import com.example.putil.PLog;import com.pui.view.MarqueeLinearLayout1;import android.app.Activity;import android.os.Bundle;import android.os.Handler;public class MainActivity extends Activity { private MarqueeLinearLayout1 marqueeLay1; @Override protected void onCreate(Bundle savedInstanceState) {  // TODO Auto-generated method stub  super.onCreate(savedInstanceState);  setContentView(R.layout.main_demo1);  marqueeLay1 = (MarqueeLinearLayout1) findViewById(R.id.marqueeLayout1); } @Override protected void onResume() {  // TODO Auto-generated method stub  super.onResume();  marqueeLay1.start(); }}

运行后我们会发现,当滚动速度比较慢的时候还能满足需求,但是当滚动速度要求比较快时(调整 private long Interval_Time 的值; ),会卡顿。主要有以下几个原因导致卡顿
1.重新layout后还会draw,耗时比较长,而且这个时间我们不可控.
2.onLayout结束后通过一个异步消息让handler再去请求重新布局。而requestLayout最终其实也是通过一个异步消息让主线程去重新布局的,
所以从onLayout结束到下一次onlayout执行主线程至少要处理了两次消息,这还是我们忽略其他消息的前提。
知道了原因,我们就得想怎么优化了。

我们知道View的重绘大概会经历measure layout draw三个过程,draw是最后一步,上面那种方式要经历layout、draw两个过程,我认为比较耗时且不好控制,那能不能只经历draw呢?接下来我们就看下面的方式,通过不断draw来实现。

二、第二种实现
在实现之前,我们先来看看draw流程
1.画当前View的背景
2.判断是否需要画渐变框,如果需要,计算渐变框相关属性
3.调用onDraw画当前视图本身的内容 我们设计的View一般会重写这个函数
4.dispatchDraw(canvas); 我们设计的View一般会重写这个函数,主要是画各个子View
5.画渐变框
6.画滚动条

那么我们需要的滚动应该重写哪个函数呢?1,2,5,6显然不是,我们的需求不涉及这些。onDraw()画视图本身,我们可以看看LinearLayout等布局视图的源码,发现它们都不会重写这个函数,都直接使用的View类的onDraw,而View的onDraw函数中没有任何代码。所以我们判断对ViewGroup而言,onDraw()没有任何意义。最后我们只剩下一种选择,4.dispatchDraw。有同学说为什么我们不直接重写draw()函数?我们当然可以这么做,但是draw函数是谷歌提供的,它内部的实现是一套标准的流程,我们为什么要去破坏它呢,比如第一步画View的背景,既然它已经有了,我们为什么还要重写而且还未必能写的比他好。

我们接着来看dispatchDraw的流程

   @Override    protected void dispatchDraw(Canvas canvas) {        final int count = mChildrenCount;        final View[] children = mChildren;        int flags = mGroupFlags;        //这部分关于动画   我们暂时不说,后面有一种方式就是利用自定义的动画去实现        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {                   ....            }        ...        mPrivateFlags &= ~DRAW_ANIMATION;        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;        boolean more = false;        final long drawingTime = getDrawingTime();      ....            for (int i = 0; i < count; i++) {                final View child = children[getChildDrawingOrder(count, i)];                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                    more |= drawChild(canvas, child, drawingTime);                }            }      ...        // mGroupFlags might have been updated by drawChild()        flags = mGroupFlags;        if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {            invalidate(true);        }     .....    }

dispatchDraw的流程除了动画的部分,关键就是遍历子View,并调用drawChild函数。

1.判断是否存在矩阵变换(矩阵变换是动画的知识,我们在下面的方式中详细说)

2.重新计算滚动值

3.根据滚动值为子视图设置canvas坐标原点。

4.根据获取的矩阵 重新设置画布

5.生成子视图的剪切图

6.绘制子视图

drawChild函数中我们重点关注下面几句

child.computeScroll(); 计算滚动值 这个函数默认是没做任何处理的
final int sx = child.mScrollX;
final int sy = child.mScrollY;

即我们如果实现computeScroll()函数,并在这个函数中改变滚动值,并让它不断重绘。那么我们改变谁的滚动值呢,每个子View的吗?
当然不是,我们改变的是外面的LinearLayout的滚动值,所以我们也不是重写child的computeScroll(),我们重写的是LinearLayout的computeScroll()函数,这个函数在LinearLayout的父视图的drawChild函数中被调用(注意是LinearLayout的父视图)

好了,我们开始我们的第二种实现。

我们前面说了 不重新layout 只是重新draw 但是不重新layout 当内容滚出界面后怎么从尾部再次滚出来呢?

来看下面几张图
假设大框为手机屏幕,中间的小块是内容
图1
这里写图片描述

这时候View1在左侧边界上,一直向左侧滚动。
图2
这里写图片描述

一直到View1的右侧滑到边界上,因为我们不想重新布局,所以我们不能将View1移到队列的尾部去,那么怎么才能显示出滚动的效果呢?
我们将往回滑动,滑动到哪个位置呢?滑到View1和图2中View2位置重叠,这时候View2,View3,View4也必然和图2中的View3,View4,View5重叠。
然后让视图继续执行从图1到图2的过程。 这样虽然视图一直在滚动,但是还做不到内容的循环,所以得将各个子视图的内容往前移一个视图的位置,在这个过程中我们还得吧View2视图的内容的移到View1视图中,View3视图的内容移到View2视图中,View4视图的内容移到View3中。。。

说的简单点 这里将视图和内容区别对待,其实View并没有循环滚动,而是不断的滚一段距离回到原位置 然后继续滚一段距离再回到原位置,只不过在回到原位置时将所有子View的内容向前搬动了一个子View ,这样给人一种错觉 就是内容在循环滚动。

图3
这里写图片描述

package com.pui.view;import android.content.Context;import android.graphics.Canvas;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.util.Log;import android.view.animation.AnimationUtils;import android.widget.LinearLayout;import android.widget.TextView;public class MarqueeLinearLayout2 extends LinearLayout { // 跑马灯的状态 是否正在滚动 private boolean MARQUEE_IS_RUNNING = false; // 是否已经初始化跑马灯滚动所需的数据 private boolean IS_INIT = false; private int childCount; private final int INIT_DATA = 0; // 记录开始滚动的时间 private long START_RUNNING_TIME = -1; // 滚动步长 单位时间内滚动的距离 private final int step = 2; // 多长时间layout一次 单位 毫秒 private long Interval_Time = 5; private static int FIRST_ITEM = 0; private int itemCount = 0; //滚动显示的内容 private String[] textStrs = { "深证:", "23.345%", "44656.5", "3.567%", "沪证",   "34.456%", "32324", "5.754%", "沪深300", "36.456%", "366624",   "9.754%" }; private Handler handler = new Handler() {  @Override  public void handleMessage(Message msg) {   // TODO Auto-generated method stub   super.handleMessage(msg);   switch (msg.what) {   case INIT_DATA:    initData();    requestLayout();    break;   default:    ;   }  } }; public MarqueeLinearLayout2(Context context, AttributeSet attrs) {  super(context, attrs);  // TODO Auto-generated constructor stub } public MarqueeLinearLayout2(Context context) {  super(context);  // TODO Auto-generated constructor stub } /**  * 父视图会调用当前视图的computeScroll()重新计算MarqueeLinearLayout2的滚动值,  * 然后根据滚动值拖动画布  * 所以我们要做的就是在computeScroll()中重新设置视图的mScrollX值,而能够改变这个值的  * 函数只有scrollTo和scrollBy  *  *  */ public void computeScroll() {  /**   * 如果跑马灯已经启动 且数据已经初始化   */  if (MARQUEE_IS_RUNNING && IS_INIT) {   /**    * 当运行到图1 或者图3的状态时,我们记录一次START_RUNNING_TIME ,这个时刻是视图回到原位置的时间    *    *  passTime 记录从恢复原位置到此刻所运行的时间    */   int passTime = (int) (AnimationUtils.currentAnimationTimeMillis() - START_RUNNING_TIME);   /**    *  我设置的速度为  每Interval_Time毫秒滚动step的像素    *  所以在passTime时间内 视图滚动了distance距离    */   int distance =(int) (step * passTime / Interval_Time);   //   //首次运行走else分支   if (START_RUNNING_TIME != -1) {    TextView textFirst = (TextView) getChildAt(0);    /**     * 获取第一个TextView的宽度,     * 如果滚动的距离已经大于这个宽度,     * 即滚动出界面了,就调用scrollTo(0,0)回到原位置     *     * FIRST_ITEM是什么?     * textStrs是内容的数组,FIRST_ITEM表示第一个子视图显示的内容在数组中的下标     * 知道了这个下标 就方便takeTextToNext()中重新设置内容了     * 这里将这个下标往后移一个位置,如果已经到了最后就重新移到第一个     *     * 最后重新记录下开始时间     *     */    if (distance >= textFirst.getWidth()) {     scrollTo(0, 0);     FIRST_ITEM = (++FIRST_ITEM % itemCount);     takeTextToNext();     START_RUNNING_TIME = (int) AnimationUtils       .currentAnimationTimeMillis();    } else {     scrollTo(distance, 0);    }   } else {    START_RUNNING_TIME = (int) AnimationUtils.currentAnimationTimeMillis();   }   // 必须调用该方法,否则不一定能看到滚动效果   postInvalidate();  }  super.computeScroll(); } public void takeTextToNext() {  Log.i("tags", "===takeTextToNext==>>===count=" + childCount);  //遍历子视图,重新设置内容 第一个子视图的下标我们已经知道  后面的在此基础加i就行 直到最后一个再回到第一个  for (int i = 0; i < childCount; i++) {   TextView view = (TextView) getChildAt(i);   view.setText(textStrs[(FIRST_ITEM + i) % itemCount]);  } } // 开始滚动跑马灯 public void start() {  handler.sendEmptyMessageDelayed(INIT_DATA, 500); } public void initData() {  MARQUEE_IS_RUNNING = true;  itemCount = textStrs.length;  childCount = getChildCount();  //初始化视图的内容  takeTextToNext();  START_RUNNING_TIME = AnimationUtils.currentAnimationTimeMillis();  IS_INIT = true; }}

布局文件main_demo2.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="vertical" ><com.pui.view.MarqueeLinearLayout2    android:id="@+id/marqueeLayout2"    android:layout_width="fill_parent"    android:layout_height="50dp"    android:orientation="horizontal"    >    <TextView         android:layout_width="100dp"        android:layout_height="wrap_content"        />     <TextView          android:layout_width="100dp"        android:layout_height="wrap_content"        />     <TextView          android:layout_width="100dp"        android:layout_height="wrap_content"        />     <TextView          android:layout_width="100dp"        android:layout_height="wrap_content"        />     <TextView          android:layout_width="100dp"        android:layout_height="wrap_content"        />     <TextView          android:layout_width="100dp"        android:layout_height="wrap_content"        /></com.pui.view.MarqueeLinearLayout2></LinearLayout>

三、动画方式实现

 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {   ....     final Animation a = child.getAnimation();        if (a != null) {            final boolean initialized = a.isInitialized();            if (!initialized) {                a.initialize(cr - cl, cb - ct, getWidth(), getHeight());                a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);                child.onAnimationStart();            }            more = a.getTransformation(drawingTime, mChildTransformation,                    scalingRequired ? mAttachInfo.mApplicationScale : 1f);            if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {                if (mInvalidationTransformation == null) {                    mInvalidationTransformation = new Transformation();                }                invalidationTransform = mInvalidationTransformation;                a.getTransformation(drawingTime, invalidationTransform, 1f);            } else {                invalidationTransform = mChildTransformation;            }            transformToApply = mChildTransformation;            concatMatrix = a.willChangeTransformationMatrix();            if (more) {                if (!a.willChangeBounds()) {                    if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==                            FLAG_OPTIMIZE_INVALIDATE) {                        mGroupFlags |= FLAG_INVALIDATE_REQUIRED;                    } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {                        // The child need to draw an animation, potentially offscreen, so                        // make sure we do not cancel invalidate requests                        mPrivateFlags |= DRAW_ANIMATION;                        invalidate(cl, ct, cr, cb);                    }                } else {                    if (mInvalidateRegion == null) {                        mInvalidateRegion = new RectF();                    }                    final RectF region = mInvalidateRegion;                    a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, invalidationTransform);                    // The child need to draw an animation, potentially offscreen, so                    // make sure we do not cancel invalidate requests                    mPrivateFlags |= DRAW_ANIMATION;                    final int left = cl + (int) region.left;                    final int top = ct + (int) region.top;                    invalidate(left, top, left + (int) (region.width() + .5f),                            top + (int) (region.height() + .5f));                }            }        } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==                FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {            final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);            if (hasTransform) {                final int transformType = mChildTransformation.getTransformationType();                transformToApply = transformType != Transformation.TYPE_IDENTITY ?                        mChildTransformation : null;                concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;            }        }        concatMatrix |= !childHasIdentityMatrix;        // Sets the flag as early as possible to allow draw() implementations        // to call invalidate() successfully when doing animations        child.mPrivateFlags |= DRAWN;        if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&                (child.mPrivateFlags & DRAW_ANIMATION) == 0) {            return more;        }        float alpha = child.getAlpha();        // Bail out early if the view does not need to be drawn        if (alpha <= ViewConfiguration.ALPHA_THRESHOLD && (child.mPrivateFlags & ALPHA_SET) == 0 &&                !(child instanceof SurfaceView)) {            return more;        }        if (hardwareAccelerated) {            // Clear INVALIDATED flag to allow invalidation to occur during rendering, but            // retain the flag's value temporarily in the mRecreateDisplayList flag            child.mRecreateDisplayList = (child.mPrivateFlags & INVALIDATED) == INVALIDATED;            child.mPrivateFlags &= ~INVALIDATED;        }        return more;    }

我们重点关注动画相关的代码
final Animation a = child.getAnimation(); 从子View中获取Animation对象,
(1)、调用Animation对象的isInitialized()方法判断Animation是否已经初始化.
(2)、如果没有初始化,调用Animation对象的initialize和initializeInvalidateRegion方法初始化。
(3)、调用Animation对象的getTransformation方法,这个方法的第一个参数为当前时间,第二个参数为Transformation类型的对象,当该方法返回时,矩阵对象可以从里面获取。返回值为true表示动画还未结束,false表示动画结束了。
(4)、如果上一步返回true,会接着调用 invalidate,表示马上要接着重绘,这样可以使动画更连贯
调用invalidate并不是意味着立马重绘,而是向主线程的MessageQueue发送了一个异步消息说我要重绘了,但是主线程其实正在忙呢,忙啥呢?当然是在draw(),我们现在不就是在draw()–>dispatchDraw()—>drawChild中, 所以只有当这次draw完成后,主线程才会再次从MessageQueue中获取消息,发现又是一次重绘。才会再次执行draw的过程。
(5)、根据动画获取的矩阵绘制。

  public boolean getTransformation(long currentTime, Transformation outTransformation,            float scale) {        mScaleFactor = scale;        return getTransformation(currentTime, outTransformation);    }  public boolean getTransformation(long currentTime, Transformation outTransformation) {        if (mStartTime == -1) {            mStartTime = currentTime;        }        final long startOffset = getStartOffset();        final long duration = mDuration;        float normalizedTime;        if (duration != 0) {            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /                    (float) duration;        } else {            // time is a step-change with a zero duration            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;        }        final boolean expired = normalizedTime >= 1.0f;        mMore = !expired;        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {         .....            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);        //我们自定义动画一班会重写这个方法            applyTransformation(interpolatedTime, outTransformation);        }        if (expired) {            if (mRepeatCount == mRepeated) {                if (!mEnded) {                    mEnded = true;                    guard.close();                    if (mListener != null) {                        mListener.onAnimationEnd(this);                    }                }            } else {                if (mRepeatCount > 0) {                    mRepeated++;                }                if (mRepeatMode == REVERSE) {                    mCycleFlip = !mCycleFlip;                }                mStartTime = -1;                mMore = true;                if (mListener != null) {                    mListener.onAnimationRepeat(this);                }            }        }        if (!mMore && mOneMoreTime) {            mOneMoreTime = false;            return true;        }        return mMore;    }

注意下面这句代码, mInterpolator这个变量是通过setInterpolator设置进去的
mInterpolator.getInterpolation(normalizedTime);
Interpolator是一个可以看成一个转换器,传入的是一个时间相关的参数,传出的是一个移动距离相关的,至于怎么转换,我们可以通过自定义Interpolator自己实现。

normalizedTime为到当前为止,动画持续的时间占整个动画需要持续的时间的比例,整个动画需要持续的时间是我们定义Animation时设置的。
如果这个比例大于等于1了,则动画有可能结束了(注意还没有确认需要结束,只是可能),expired = normalizedTime >= 1.0f;

如果比例大于1,在判断要求的重复次数和当前已经重复的次数是否一致,一致那就真的结束了,如果不一致,那就将mStartTime = -1; mMore = true; 表示动画还未真正结束,还得重复。
mRepeatCount这个值当前类搜索可以知道是通过setRepeatCount这个函数设置的。

  if (expired) {            if (mRepeatCount == mRepeated) {                if (!mEnded) {                    mEnded = true;                    guard.close();                    if (mListener != null) {                        mListener.onAnimationEnd(this);                    }                }            } else {                if (mRepeatCount > 0) {                    mRepeated++;                }                if (mRepeatMode == REVERSE) {                    mCycleFlip = !mCycleFlip;                }                mStartTime = -1;                mMore = true;                if (mListener != null) {                    mListener.onAnimationRepeat(this);                }            }

我们现在需要实现滚动,那就是要对所有的子View添加一个左方向移动的动画了。来看看我是怎么自定义动画的。

android系统自带有平移的动画类,叫做TranslateAnimation,但是它并不适用我们的需求,我们这要求循环滚动。虽然不适合,但是我们完全可以在它的基础上修改。我们学习一样新东西不就是从模仿开始的吗。

package com.example.marqueedemo.anim;import android.view.animation.Animation;import android.view.animation.LinearInterpolator;import android.view.animation.Transformation;public class MyTranslateAnimation extends Animation { private int mFromXType = ABSOLUTE; private int mToXType = ABSOLUTE; private float mFromXValue = 0.0f; private float mToXValue = 0.0f; private float viewlen = 0.0f; private float mFromXDelta; private float mToXDelta; private float lend; public MyTranslateAnimation(float fromXDelta, float toXDelta,   float fromYDelta, float toYDelta) {  mFromXValue = fromXDelta;  mToXValue = toXDelta;  mFromXType = ABSOLUTE;  mToXType = ABSOLUTE; } public MyTranslateAnimation(int fromXType, float fromXValue, int toXType,   float toXValue, int fromYType, float fromYValue, int toYType,   float toYValue) {  mFromXValue = fromXValue;  mToXValue = toXValue;  mFromXType = fromXType;  mToXType = toXType; } public void setWidth(int width){  viewlen = width; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) {  float dx = 0f;  float distance = lend *interpolatedTime;  if(distance < (-mToXDelta)){   dx=mFromXDelta-distance;  }else{   dx=mFromXDelta+lend - distance;  }  t.getMatrix().setTranslate(dx, 0); } @Override public void initialize(int width, int height, int parentWidth,   int parentHeight) {  super.initialize(width, height, parentWidth, parentHeight);  mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);  mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);  lend = resolveSize(mFromXType, viewlen, width, parentWidth);  setInterpolator(new LinearInterpolator());  //setFillAfter(true);  setRepeatCount(-1); }}

先来想一下我们的动画和普通的移动动画有什么区别
1.无限循环
2.从左边滚动出界后,会再次从右侧滚出

initialize函数中,我们添加setInterpolator(new LinearInterpolator()); 因为我们的滚动是匀速的,也就是说我们前面mInterpolator.getInterpolation(normalizedTime); normalizedTime和返回的值是成正比的。
setRepeatCount(-1); 设置重复次数,当次数为负数时,将会无限循环。
lend为整个可滚动的视图宽度,包括屏幕外的那部分,也就是12个TextView的宽度 我们是通过最后一个TextView的getRight获取的

applyTransformation中 lend *interpolatedTime; 为一共滚动的距离,如果这个距离小于-mToXDelta,即还没有超出左侧边界,
dx=mFromXDelta-distance; 如果超出了,那就dx=mFromXDelta+lend - distance;

动画定义好了 我们怎么用呢?

package com.pui.view;import com.example.marqueedemo.anim.MyTranslateAnimation;import com.example.putil.PLog;import android.content.Context;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.util.Log;import android.widget.LinearLayout;import android.widget.TextView;public class MarqueeLinearLayout3 extends LinearLayout { private int childCount; private final int INIT_DATA = 0; private static int FIRST_ITEM = 0; private int itemCount = 0; // 滚动显示的内容 private String[] textStrs = { "深证:", "23.345%", "44656.5", "3.567%", "沪证",   "34.456%", "32324", "5.754%", "沪深300", "36.456%", "366624",   "9.754%" }; public MarqueeLinearLayout3(Context context, AttributeSet attrs) {  super(context, attrs);  // TODO Auto-generated constructor stub } public MarqueeLinearLayout3(Context context) {  super(context);  // TODO Auto-generated constructor stub } private Handler handler = new Handler() {  @Override  public void handleMessage(Message msg) {   // TODO Auto-generated method stub   super.handleMessage(msg);   switch (msg.what) {   case INIT_DATA:    initData();    requestLayout();    break;   default:    ;   }  } }; public void takeTextToNext() {  Log.i("tags", "===takeTextToNext==>>===count=" + childCount);  // 遍历子视图,重新设置内容 第一个子视图的下标我们已经知道 后面的在此基础加i就行 直到最后一个再回到第一个  for (int i = 0; i < childCount; i++) {   TextView view = (TextView) getChildAt(i);   view.setText(textStrs[(FIRST_ITEM + i) % itemCount]);  } } // 开始滚动跑马灯 public void start() {  handler.sendEmptyMessageDelayed(INIT_DATA, 500); } public void initData() {  itemCount = textStrs.length;  childCount = getChildCount();  // 初始化视图的内容  takeTextToNext();  int width = getChildAt(childCount-1).getRight();  PLog.i("tags", "width==" + width);//为所有子View添加动画  loadAnimations(width); } private void loadAnimations(int width) {  for (int i = 0; i < childCount; i++) {   TextView view = (TextView) getChildAt(i);   int right = view.getRight();   MyTranslateAnimation trans = new MyTranslateAnimation(0f,     -right, 30f, 300f);   trans.setDuration(5000);   trans.setWidth(width);   view.setAnimation(trans);   PLog.i("tags",     "mleft===" + view.getLeft() + "===right=" + view.getRight()       + "===count=" + childCount);  } }}

动画方式的实现也讲完了,这种方式的缺点是为每个子View添加了各自的动画,增加了运算量,而且对每个View的动画处理也要注意之间的衔接,毕竟我们要滚动的是整个视图,但动画却是添加在子View上。

这里三种方式我并没有做过多的测试,而且我确定一定存在兼容性问题。在这里讲这么多重点是分析怎么自定义一个View 怎么重写它的方法,以及理解View重绘的原理。这篇就到此结束了,TextView好像有两个属性android:ellipsize=”marquee” android:marqueeRepeatLimit=”marquee_forever”也有跑马灯的效果,但是好像速度不可调,且会停顿。我暂时还未来得及看那TextView那一块的源码,打算接下去几天看看,然后在下一篇中通过类似的途径实现跑马灯。

1 0
原创粉丝点击