• 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

  • 相关阅读:
    NSString常用方法
    iOS8 【xcode6中添加pch全局引用文件】
    Size Classes with Xcode 6
    推荐学习网站
    Objective C类方法load和initialize的区别
    iOS开发中,单元测试的好处!
    iOS 9.0中UIAlertController的用法。
    iOS 9.0 数组中所有对象共同执行方法的接口不可用
    Xcode 7.0以后 iOS 9.0以后 最新百度地图 详细使用
    iOS9适配关于URL Schemes
  • 原文地址:https://www.cnblogs.com/sishuiliuyun/p/14584705.html
Copyright © 2020-2023  润新知