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那一块的源码,打算接下去几天看看,然后在下一篇中通过类似的途径实现跑马灯。
- AndroidView绘制流程分析及自定义View、ViewGroup进阶
- AndroidView绘制流程与源码分析
- Android View 绘制流程 及 自定义View
- Android 自定义View、ViewGroup(二)之绘制流程
- Android进阶——自定义View之View的绘制流程及实现onMeasure完全攻略
- 自定义View及ViewGroup
- android-进阶(3)-自定义view(2)-Android中View绘制流程以及相关方法的分析
- Android学习自定义View(二)——View和ViewGroup绘制流程以及invalidate()
- View和ViewGroup的基本绘制流程
- View、ViewGroup的测量、布局、绘制流程
- View绘制流程(3)----view的绘制流程及自定义View的相关问题
- Android的自定义View及View的绘制流程
- 自定义View绘制心得(自定义view和自定义viewGroup)
- Android 自定义View基本绘制流程及实例
- Android自定义View或ViewGroup的流程
- 自定义view的绘制流程
- 自定义VIEW②绘制流程
- android 自定义view绘制流程
- 数据源浏览时提示‘cs0016:未能写入输出文件 拒绝访问’是怎么回事?
- 实现圆形加载中效果自定义Dialog
- 【Mockplus视频教程】《10分钟玩转Mockplus》
- Lua语言介绍
- 同一字符串显示不同风格
- AndroidView绘制流程分析及自定义View、ViewGroup进阶
- git使用总结
- c#基础之数组
- Linux按时间排序文件
- Leetcode#67||Add Binary
- 机房收费系统之组合查询
- 如何查看存储过程中动态生成的sql
- Jmeter 在 linux 命令行下报“获取连接时间过长”的异常
- Android更改 PreferenceFragment 的背景颜色