这里将通过一个需求的几种不同的实现来给大家分析View的自定义。
源代码下载地址
[http://download.youkuaiyun.com/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那一块的源码,打算接下去几天看看,然后在下一篇中通过类似的途径实现跑马灯。