Android View学习笔记(四):Scroller的原理剖
一、前言
在上一篇文章中,笔者讲述了Scroller的模板代码以及其原理,对它和View的重绘进行了分析,知道了原理后,这篇文章将结合一个Demo来讲述其用法,以加强读者对Scroller的掌握程度。
二、实例
我们先看该实例的效果是怎样的:
根据图可以看出,当点击按钮后,小球从高处滑落至底部,并且在底部会反弹,我们使用Scroller来实现以上效果。
(1)首先,我们先绘制小球,自定义一个View,在其onDraw()方法完成绘制,以下为ViewA:
public class ViewA extends View {
private final int radius = 50;
public int getRadius() {
return radius;
}
public ViewA(Context context) {
super(context);
}
public ViewA(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 1、先实例化一个Paint对象,该对象充当“画笔”的作用
* 2、设置抗锯齿、画笔颜色等,这里填充为蓝色
* 3、调用canvas的drawCircle方法绘制圆形,
* 第1、2个参数表示坐标,第3个参数表示半径
*/
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.BLUE);
canvas.drawCircle(50,50,radius,paint);
}
}
(2)接着,由于我们要对这个小球(ViewA)滑动,那么又因为Scroller是对一个View的内容进行滑动的,那么我们自然就会想到可以在这个ViewA外包裹一层LinearLayout,这样对这个LinearLayout进行Scroller滑动,那么里面的ViewA就会跟着滑动了,这里我们新建一个ParentView,继承LinearLayout:
public class ParentView extends LinearLayout {
private Scroller mScroller;
private ViewA viewA;
private int realHeight;
public ParentView(Context context) {
super(context);
}
public ParentView(Context context, AttributeSet attrs) {
super(context, attrs);
//为了实现回弹效果,这里传递一个BounceInterpolator插值器,该插值器专门用于实现回弹效果
mScroller = new Scroller(context, new BounceInterpolator());
}
/**
* 初始化ScrollX、ScrollY,同时获取子View的实例,获取其半径参数
*
* startScroll(int startX, int startY, int dx, int dy, int duration)方法:
* startX、startY表示滑动开始的坐标;dx、dy表示需要位移的距离;duration表示移位的时间
*
* invalidate()方法:在View树重绘的时候会调用computeScrollOffset()方法
*/
public void smoothScrollTo(){
viewA = (ViewA) getChildAt(0);
int ScrollX = getScrollX();
int ScrollY = getScrollY();
realHeight = getHeight()-2*viewA.getRadius();
mScroller.startScroll(ScrollX, 0, 0, -realHeight, 1000);
invalidate();
}
/**
* 先调用computeScrollOffset()方法,计算出新的CurrX和CurrY值,
* 判断是否需要继续滑动。
*
* scrollTo(currX,currY):滑动到上面计算出的新的currX和currY位置处
*
* postInvalidate():通知View树重绘,作用和invalidate()方法一样
*/
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
Log.d("cylog", "滑动坐标"+"("+getScrollX()+","+getScrollY()+")");
scrollTo(currX, currY);
postInvalidate();
}
}
}
(3)MainActivity:这里主要执行布局的初始化以及监听按钮的点击事件:
public class MainActivity extends Activity {
private Button button;
private ParentView parentView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
button = (Button) findViewById(R.id.button);
parentView = (ParentView) findViewById(R.id.parentView);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//对parentView的内容进行滑动
parentView.smoothScrollTo();
}
});
}
}
(4)最后,我们看xml布局文件,这里要注意的是:我们引入了自定义布局,那么在xml布局就应该显式写出包名.类名,否则会出错,如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始下落"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp" />
<com.example.administrator.scroller.ParentView
android:id="@+id/parentView"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/button">
<com.example.administrator.scroller.ViewA
android:id="@+id/viewA"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignTop="@+id/view"
android:layout_alignRight="@+id/button"
android:layout_alignEnd="@+id/button"/>
</com.example.administrator.scroller.ParentView>
</RelativeLayout>
完成了所有代码的编写后,运行测试,就会显示一开始说的效果了。
三、遇到的问题
笔者在学习Scroller的时候,由于Scroller涉及到了View的绘制原理,所以有时候会对View的重绘感到困惑,这里与大家分享我学习过程中遇到的一个问题。
在上一篇文章中,笔者有说到:在View#draw()方法中,绘制一个View有6个步骤,其中step 3中调用到onDraw()方法,说明一个View的重绘理论上是会调用到重写的onDraw()方法的,于是笔者在ParentView的onDraw()方法内打印了日志,看看是否真的会调用这个方法。但结果与分析不同,没有调用到onDraw()方法,为什么呢?经过查找了很多资料,终于知道了答案了。原来在一个View中,有这样一个方法:View#setWillNotDraw(boolean willNotDraw)
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从注释我们了解到,如果一个View不需要绘制任何内容,那么系统会对View的绘制进行优化,即不会调用到onDraw()方法,而系统判定是否需要进行优化的参数是willNotDraw。默认地,一个View继承了Viwe则这个参数设置为false,此时不优化;但一个ViewGroup默认会设置willNotDraw为true,即View树重绘的时候不会调用到ViewGroup的onDraw()方法。这也就解释了我的疑问,为什么在滑动的时候,进行了View树的重绘而ViewGroup的onDraw()方法始终没有调用。所以,如果要使ViewGroup的onDraw()方法得到调用,那么我们在实例化这个ViewGroup的时候应该调用这个方法:setWillNotDraw(false),设置不对ViewGroup进行优化,或者这样:为ViewGroup设置一个background属性(xml布局中),那么系统就会认为该ViewGroup存在内容了,此时就会每一次都调用onDraw()方法了。
解决了以上这个问题后,那么再引申出这样一个问题:ViewGroup重绘的时候,子View的onDraw()方法有没有调用呢?从理论上分析,我们在调用ViewGroup的重绘的时候是会调用到子View的draw()方法的,在draw()方法的内部又会调用onDraw()方法的,因此我们可以在子View的onDraw()方法内打印一下日志,事实上,在当前的Scroller背景下,子View的onDraw()方法是没有被调用的,但这个和上面说到的willNotDraw没有关系,因为子View是默认不开启优化的,那么到底为什么呢?其实在View的内部有一个标志参数,用来标志当前View是否需要重绘,如果这个View的内容没有改变,那么系统就会认为这个View不需要重新绘制,所以就不会调用子View的onDraw()方法了,由于当前的Scroller方法并没有对子View的内容作用,因此子View最终也没有调用这个onDraw()方法。以上为本人的一点见解,如果说错了,还望指正。还有,谢谢看到这里的你。