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()方法。以上为本人的一点见解,如果说错了,还望指正。还有,谢谢看到这里的你。

作者:丶蓝天白云梦
链接:https://www.jianshu.com/p/c8657df404b2

原文地址:https://www.cnblogs.com/sishuiliuyun/p/14584705.html