zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Golang Failpoint 的设计与实现
    没涉及到最值求解;观点:矩阵乘法无法表达出结果。 现实生活中事件、现象的数学表达
    多元微分学 枚举破解15位路由器密码 存储空间限制 拆分减长,求最值 数据去重
    ARP Poisoning Attack and Mitigation Techniques ARP欺骗 中间人攻击 Man-In-The-Middle (MITM) attack 嗅探 防范 Can one MAC address have two different IP addresses within the network?
    The C10K problem
    HTTP Streaming Architecture HLS 直播点播 HTTP流架构
    现代IM系统中消息推送和存储架构的实现
    现代IM系统中的消息系统架构
    长连接锁服务优化实践 C10K问题 nodejs的内部构造 limits.conf文件修改 sysctl.conf文件修改
    doubleclick cookie、动态脚本、用户画像、用户行为分析和海量数据存取 推荐词 京东 电商 信息上传 黑洞 https://blackhole.m.jd.com/getinfo
  • 原文地址:https://www.cnblogs.com/sishuiliuyun/p/14584705.html
Copyright © 2011-2022 走看看