Android-自定义ViewGroup-上下滑动整体实践下

本来上周六晚上出去散步的时候就随便想了下,当时的想法是ViewGroup要实现内部控件的滚动,1. 最终效果肯定就是子控件的重绘对吧? 2. 重绘肯定就涉及到onLayout重新定位的处理对吧? 重新定位+重新绘制理论上就是实现滚动的原理了吧。

基于上述猜测,小白以为我们只要在onLayout中重新刷新控件的位置不就可以实现滚动了么?没错,小白实践了,可以滴?--需要了解如下知识:

Invalidate:
To farce a view to draw,call invalidate().——摘自View类源码
从上面这句话看出,invalidate方法会执行draw过程,重绘View树。
当View的appearance发生改变,比如状态改变(enable,focus),背景改变,隐显改变等,这些都属于appearance范畴,都会引起invalidate操作。

所以当我们改变了View的appearance,需要更新界面显示,就可以直接调用invalidate方法。

View(非容器类)调用invalidate方法只会重绘自身,ViewGroup调用则会重绘整个View树。

RequestLayout:
To initiate a layout, call requestLayout(). This method is typically called by a view on itself when it believes that it can no longer fit within its current bounds.——摘自View源码

从上面这句话看出,当View的边界,也可以理解为View的宽高,发生了变化,不再适合现在的区域,可以调用requestLayout方法重新对View布局。

View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。

requestLayout()方法就是我们请求重新测量和定位的方式。

**1. **比如我们实现一个简单的效果有问题的上下滑动的ViewGroup

1.1 首先获取上下滑动事件处理

1.2 获得上下滑动分别的距离

1.3 滑动的距离分别附加到onlayout的leftTop上,也就是原来控件的摆放的初始位置全部加上这个偏移量(该偏移量如果是向上滑动为负数, 向下自然就是整数;该便宜是累计的,在onTouchEvent中做简单计算哟)

So, 看简单的偏移量的计算....应该不难吧...

    private float x1, x2;
    private float y1, y2;
    private float swing = 0;
    private float totalswings = 0;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                x1 = event.getX();
                y1 = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x2 = event.getX();
                y2 = event.getY();

                ///< 5作为阀值就可以了,可以根据效果调整
                if(y1 - y2 > 5) {         ///< 上
                    swing = y1 - y2;
                    totalswings += swing;
                    requestLayout();

                    Log.e("test", "上滑动");
                } else if(y2 - y1 > 5) { ///< 下
                    swing = -(y2 - y1);
                    totalswings += swing;
                    requestLayout();

                    Log.e("test", "下滑动");
                } else if(x1 - x2 > 50) { ///< 左
                } else if(x2 - x1 > 50) { ///< 右
                }
                x1 = x2;
                y1 = y2;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

Then, 拿到偏移累计后totalswings,就可以在onLayout中进行偏移添加:

image

效果就是这么粗糙啦(上下错了这些不要在意,关键是我们知道这样可以做,具体效果估计要搞一些好的算法处理才行)..

image

2. 或许我们可以像上面那样做,但是感觉有点麻烦的样子...再查看一些个资料,发现其实大部分码友的做法是调用系统的scroll相关的方法 - 看官方

scrollBy
added in API level 1
public void scrollBy (int x, 
                int y)
Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

Parameters
x   int: the amount of pixels to scroll by horizontally
y   int: the amount of pixels to scroll by vertically
scrollTo
added in API level 1
public void scrollTo (int x, 
                int y)
Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

Parameters
x   int: the x position to scroll to
y   int: the y position to scroll to

scrollBy、scrollTo,两个来自View的方法。ViewGroup继承自View,自然也可以直接调用该方法。

scrollBy - 滚动的距离(用这个来实现滚动的效果即可,每次onTouch我们都计算了滑动偏移量)

scrollTo - 滚动到哪里

So, 我们改造下onTouchEvent方法:

    private float x1, x2;
    private float y1, y2;
    private float swing = 0;
    private float totalswings = 0;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                x1 = event.getX();
                y1 = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x2 = event.getX();
                y2 = event.getY();

                ///< 5作为阀值就可以了,可以根据效果调整
                if(y1 - y2 > 5) {         ///< 上
                    swing = y1 - y2;
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    scrollBy(0, (int) swing);

                    Log.e("test", "上滑动");
                } else if(y2 - y1 > 5) { ///< 下
                    swing = -(y2 - y1);
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    scrollBy(0, (int) swing);

                    Log.e("test", "下滑动");
                } else if(x1 - x2 > 50) { ///< 左
                } else if(x2 - x1 > 50) { ///< 右
                }
                x1 = x2;
                y1 = y2;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

效果一般般啦:

image

2.1 稍微看下scrollBy源码吧....

     public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

最后调用的其实是scrollTo

     public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

关注下下面这两个变量哈....

image

再往每个具体的函数跟,其实还是复杂。我们换个思路,既然最终是靠定位和重绘实现的滚动,那么我们去看看TextView的onDraw:

image

小白框起来的些个变量,有没有想到一些联系(小白大体跟了下:当调用scrollBy时,会更新对应的mScrollX/Y,然后请求相关控件的重定位,重绘,进而达到滚动效果)。。。其实子控件再考虑绘制的时候已经考虑到了父控件相关的滚动设计。 当然要彻底搞明白整个流程,肯定不是一天两天的事情(对小白来说).

不过对小白来讲,能学到了解到上面些的知识,还是蛮不错的。原理还是有点领悟的....

**3. 当然如果你搜索网上相关的资料,会发现有些码友用的是Scroller - 所谓的弹性辅助滑动类 **Scroller | Android Developers

Scroller就是一个滑动帮助类。它并不可以使View真正的滑动,
而是配合scrollTo/ScrollBy让view产生缓慢的滑动,产生动画的效果,其实和属性动画是同一个原理。
在我看来,Scroller跟属性动画的平移的效果是一样的。

我们可以试试(具体的用法和效果可以专门学习下,然后用到我们的自定义ViewGroup中).

使用方式是这样的:

   To track the changing positions of the x/y coordinates,
 use computeScrollOffset(). 
The method returns a boolean to indicate whether the scroller is finished.
 If it isn't, it means that a fling or programmatic pan operation is still in progress. 
You can use this method to find the current offsets of the x and y coordinates, for example:
if (mScroller.computeScrollOffset()) {
     // Get current x and y positions
     int currX = mScroller.getCurrX();
     int currY = mScroller.getCurrY();
    ...
 }

3.1 调用mScroller.startScroll(0, 0, 100, 0);
// Invalidate to request a redraw
invalidate();

3.2. 根据上面解释,此时就会触发computeScroll()方法的回调:

然后就可以进行类似如下的处理scrollTo、scrollBy看你需要:

   @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

比如我们这样的处理,当然效果肯定不对,不过先不管了哟!

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                x1 = event.getX();
                y1 = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x2 = event.getX();
                y2 = event.getY();

                ///< 5作为阀值就可以了,可以根据效果调整
                if(y1 - y2 > 5) {         ///< 上
                    swing = y1 - y2;
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    //scrollBy(0, (int) swing);
                    mScroller.startScroll(0, 0, 0, (int) swing);
                    invalidate();

                    Log.e("test", "上滑动");
                } else if(y2 - y1 > 5) { ///< 下
                    swing = -(y2 - y1);
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    //scrollBy(0, (int) swing);
                    mScroller.startScroll(0, 0, 0, (int) swing);
                    invalidate();

                    Log.e("test", "下滑动");
                } else if(x1 - x2 > 50) { ///< 左
                } else if(x2 - x1 > 50) { ///< 右
                }
                x1 = x2;
                y1 = y2;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    } 

效果:基本滑不动,哈哈~~~先飘过,不管了....

综上我们就大概对这个滚动有了总体的认识、以及对scroll和Scroller的综合理解。作为知识进阶,总得了解更多,虽然远远不够!!

有个疑问需要提下?后面具体研究估计才懂,有懂的可以指教下 - 就是为什么滚动了,但是onLayout并没有进行回调,那怎么实现的这个子控件的重定位,难道仅仅是重绘?

最后看下整体源码吧:CustomViewGroupLastNew.java

package me.heyclock.hl.customcopy;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/*
 *@Description: 自定义ViewGroup + 纵向垂直布局 + 单列 + 粗糙上下滑动
 *@Author: hl
 *@Time: 2018/10/25 10:18
 */
public class CustomViewGroupLastNew extends ViewGroup {
    private Context context;///< 上下文
    /**
     * 计算子控件的布局位置.
     */
    private final Rect mTmpContainerRect = new Rect();
    private final Rect mTmpChildRect = new Rect();
    /**
     * 滚动相关(上下滑动)
     */
    private float x1, x2;
    private float y1, y2;
    private float swing = 0;
    private float totalswings = 0;
    private Scroller mScroller;

    public CustomViewGroupLastNew(Context context) {
        this(context, null);
    }

    public CustomViewGroupLastNew(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, 0, 0);
    }

    public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;
        mScroller = new Scroller(context);
    }

    /**
     * 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ///< 定义最大宽度和高度
        int maxWidth = 0;
        int maxHeight = 0;
        ///< 获取子控件的个数
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View view = getChildAt(i);
            ///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量
            if (view.getVisibility() != GONE) {
                ///< 获取子控件的属性 - margin、padding
                CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
                ///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
                ///<  - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width);
                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height);
                ///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
                view.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                ///< 然后再次获取测量后的子控件的属性
                layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
                ///< 然后获取宽度的最大值、高度的累加
                maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin);
                maxHeight += view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
            }
        }

        ///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
        maxWidth = Math.max(maxWidth, getMinimumWidth());
        maxHeight = Math.max(maxHeight, getMinimumHeight());

        ///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        ///< wrap_content的模式
        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(
                    maxWidth + getPaddingLeft() + getPaddingRight(),
                    maxHeight + getPaddingTop() + getPaddingBottom());
        }
        ///< 精确尺寸的模式
        else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
            setMeasuredDimension(wSize, hSize);
        }
        ///< 宽度尺寸不确定,高度确定
        else if (wSpecMode == MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), hSize);
        }
        ///< 宽度确定,高度不确定
        else if (hSpecMode == MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(wSize, maxHeight + getPaddingTop() + getPaddingBottom());
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.e("test", "onLayout(" + left + "," + top + "," + right + "," + bottom + ")");
        ///< 获取范围初始左上角 - 这个决定子控件绘制的位置,我们绘制理论可以从0,0开始,margin容器本身已经考虑过了...所以别和margin混淆了
        int leftPos = getPaddingLeft();
        int leftTop = getPaddingTop() + (int)totalswings;
        ///< 获取范围初始右下角 - 如果考虑控件的位置,比如靠右,靠下等可能就要利用右下角范围来进行范围计算了...
        ///< 后面我们逐步完善控件的时候用会用到这里...
        //int rightPos = right - left - getPaddingRight();
        //int rightBottom = bottom - top - getPaddingBottom();

        ///< 由于我们是垂直布局,并且一律左上角开始绘制的情况下,我们只需要计算出leftPos, leftTop就可以了
        int count = getChildCount();
        for (int i = 0; i < count; ++i){
            View childView = getChildAt(i);
            ///< 控件占位的情况下进行计算
            if (childView.getVisibility() != GONE){
                ///< 获取子控件的属性 - margin、padding
                CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) childView.getLayoutParams();

                int childW = childView.getMeasuredWidth();
                int childH = childView.getMeasuredHeight();

                ///< 先不管控件的margin哈!
                int cleft = leftPos + layoutParams.leftMargin;
                int cright = cleft + childW;
                int ctop = leftTop + layoutParams.topMargin;
                int cbottom = ctop + childH;

                ///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!
                leftTop += childH + layoutParams.topMargin + layoutParams.bottomMargin;

                ///< 需要一个范围,然后进行摆放
                childView.layout(cleft, ctop, cright, cbottom);
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            ///< 获取子控件的宽高
            View view = getChildAt(i);
            Log.e("test", "getPaddingLeft()=" + view.getPaddingLeft());
            Log.e("test", "getPaddingRight()=" + view.getPaddingRight());
            Log.e("test", "getPaddingTop()=" + view.getPaddingTop());
            Log.e("test", "getPaddingBottom()=" + view.getPaddingBottom());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                x1 = event.getX();
                y1 = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x2 = event.getX();
                y2 = event.getY();

                ///< 5作为阀值就可以了,可以根据效果调整
                if(y1 - y2 > 5) {         ///< 上
                    swing = y1 - y2;
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    scrollBy(0, (int) swing);
                    ///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动
                    //mScroller.startScroll(0, 0, 0, (int) swing);
                    invalidate();

                    Log.e("test", "上滑动");
                } else if(y2 - y1 > 5) { ///< 下
                    swing = -(y2 - y1);
                    ///< 采用刷新onLahyout实现滚动
                    //totalswings += swing;
                    //requestLayout();
                    ///< 采用系统View类的滚动方法
                    scrollBy(0, (int) swing);
                    ///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动
                    //mScroller.startScroll(0, 0, 0, (int) swing);
                    invalidate();

                    Log.e("test", "下滑动");
                } else if(x1 - x2 > 50) { ///< 左
                } else if(x2 - x1 > 50) { ///< 右
                }
                x1 = x2;
                y1 = y2;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    @Override
    public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);
    }

    /**
     * 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
     * --目的是获取自定义属性以及一些使用常量的自定义
     */
    public static class LayoutParams extends MarginLayoutParams {
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            // Pull the layout param values from the layout XML during
            // inflation.  This is not needed if you don't care about
            // changing the layout behavior in XML.
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
            ///< TODO 一些属性的自定义
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

Next,我看下点击事件以及后续的滑动冲突的处理....快了,快把自定义流程简单过一遍了,加油,么么哒!

你可能感兴趣的:(Android-自定义ViewGroup-上下滑动整体实践下)