WebView与ListView滑动冲突——(二)LinearLayout控制WebView滚动

上一篇我们大致了解了一下View中 事件的一些基础《WebView与ListView滑动冲突——(一)事件基础篇》Scroll是为了实现View平滑滚动的一个Helper类,通常在自定义View中使用。 这次我们从一下几个方面来了解一下Scroll与VelocityTracker的用法:

  • View和MotionEvent的位置信息
  • View中的Scroll方法
  • Scroll中的scroll*()方法
  • touchSlop与VelocityTracker
  • LinearLayout控制WebView滚动

通过上面几个方面的学习,将会对View的滑动有一定的认知。废话不多说了,开始

View和MotionEvent中的位置信息

在我们自定义View中,经常要获取各种位置坐标,但是View的get方法那么多,我也不知道这个get方法获取的到底是不是那个位置坐标,我能怎么办,我也很绝望呀。所以就整理了一些View和MotionEvent中的一些获取位置信息的一些方法。

View的位置信息

View中的获取位置信息的get方法。有些是不能直接在Activity的onCreate()中调用,因为当时View还未绘制完成,这个时候调用View的get方法,获取到的位置信息当然是0了。一般可以在Activity的onWindowFocusChanged()方法中获取,或者使用延迟策略去获取。

下面我们来看一下View中的通过那些信息来定位View的位置

left、right、top、bottom、elevation

这五个参数表示的是View的原始位置距离父控件边缘的距离,并且无论这个View被移动到了什么位置,或者被缩放、旋转了多少,这五个值都是永久不变的

  • left:目标View的最左边和这个View所在父控件的最左边的距离,通过view.getLeft()方法获取;
  • right:目标View的最右边和这个View所在父控件的最左边的距离,通过view.getRight()方法获取;
  • top:目标View的最上边和这个View所在父控件的最上边的距离,通过view.getTop()方法获取;
  • bottom:目标View的最下边和这个View所在父控件的最上边的距离,通过view.getBottom()方法获取;
  • elevation:目标View的Z轴高度和这个View所在的父控件所在的Z轴高度的距离,通过view.getElevation()方法获取(这个属性是Android 5.0之后添加的新属性)

撒?你说还是不明白,没事。我也没明白。还是用一张图来认识一下把:

这里写图片描述

translationX、translationY、translationZ

这三个参数代表的是在动画或者滑动View的时候,View的当前位置相对于其原始位置平移的距离:

  • translationX:在滑动过程中,View当前位置的最左边和这个View原始位置的最左边的距离,通过view.getTranslationX()方法获取;
  • translationY:在滑动过程中,View当前位置的最上边和这个View原始位置的最上边的距离,通过view.getTranslationY()方法获取;
  • translationZ:在动画过程中,View当前位置的Z轴高度和这个View原始Z轴高度的距离,通过view.getTranslationZ()方法获取(这个方法是Android 5.0之后添加的新方法)。
x、y、z

这三个参数代表的是View的当前位置相对于其父控件的距离:

  • x:目标View的当前位置的最左边和这个View所在父布局的最左边的距离,通过view.getX()方法获取;
  • y:目标View的当前位置的最上边和这个View所在父布局的最上边的距离,通过view.getY()方法获取;
  • z:目标View的当前位置的Z轴位置和这个View所在父布局的Z轴位置的距离,通过view.getZ()方法获取(这个方法是Android 5.0之后添加的新方法)。

这三个参数和前面的几个参数的关系公式如下:

x = left + translationX;
y = top + translationY;
z = elevation + translationZ;

MotionEvent的位置信息

使用MotionEvent类,我们还可以获取到触摸屏幕时View的一些位置参数:

  • x:当前触摸的位置相对于目标View的X轴坐标,通过getX()方法获取;
  • y:当前触摸的位置相对于目标View的Y轴坐标,通过getY()方法获取;
  • rawX:当前触摸的位置相对于屏幕最左边的X轴坐标,通过getRawX()方法获取;
  • rawY:当前触摸的位置相对于屏幕最上边的Y轴坐标,通过getRawY()方法获取。

下面也通过一张图来清楚的认识一下把,要不然看完还是懵逼+懵逼=懵逼²。。。

这里写图片描述

好了。对于View和MotionEvent的位置信息就简单说这么多把,够用就行。再说下去就 下笔千言离题万里了(关键是就知道这么多了- -!)

View中的Scroll方法

Android中为了实现View的滑动,系统为此提供了scrollTo()和scrollBy()两个方法。打开源码来让我们一探究竟。

    /** 
     * Set the scrolled position of your view. This will cause a call to 
     * {@link #onScrollChanged(int, int, int, int)} and the view will be 
     * invalidated. 
     * @param x the x position to scroll to 
     * @param y the y position to scroll to 
     */  
    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();  
            }  
        }  
    }  
    /** 
     * Move the scrolled position of your view. This will cause a call to 
     * {@link #onScrollChanged(int, int, int, int)} and the view will be 
     * invalidated. 
     * @param x the amount of pixels to scroll by horizontally 
     * @param y the amount of pixels to scroll by vertically 
     */  
    public void scrollBy(int x, int y) {  
        scrollTo(mScrollX + x, mScrollY + y);  
    }  

scrollTo()方法的注释中,我们可以看到,这个方法将会调用onScrollChanged()方法并且这个View将会被重绘,这就达到了View的内容位置变化效果。而scrollBy(),这货就比较懒了,内部直接调用了scrollTo()方法。不过两者的区别还是很明显的,scrollTo()是直接一步到位,而scrollBy()则是慢慢的累积。

mScrollX和mScrollY则是View的偏移量。而且都是指当前view的内容相对view本身左上角起始坐标的偏移量。看了下面这幅图你就明白了:

这里写图片描述

来个小结:

  • scrollTo()的移动是一步到位,而scrollBy()逐步累加的
  • scrollTo()和scrollBy()传递的参数是偏移量而非坐标
  • scrollTo()和scrollBy()移动的都只是View的内容,View的背景本身是不移动的。

    注:在实际操作中你会发现,传入的参数完全跟想执行的操作相反。所以如果我们想往x轴和y轴正方向移动时,mScrollY和mScrollX必须为负值,相反如果我们想往x轴和y轴负方向移动时,mScrollY和mScrollX就必须为正值

TouchSlop与VelocityTracker

TouchSlop

在自定义View中,我们有时会用到下面这行代码:

mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  

那么这个方法获取到的int值到底是个啥啊?通过方法上面的注释才知道,TouchSlop是一个滑动距离的常量值,并且需要注意的是不同的设备,touchSlop的值可能是不同的,一切以上述的函数获取为准。

这个值对于自定义View有什么用呢?我们来举个例子,当你按下屏幕后,不上下滑动,也不松开,而是手指左右上下抖一抖,会一直触发ACTION_MOVE事件,只有当滑动距离大于TouchSlop这个值时,系统才认为我们做了滑动操作。

VelocityTracker

VelocityTracker是一个滑动速率的Helper类。它可以辅助跟踪触摸事件的速率,比如快速滑动或者其他的滑动手势。我们一般会在ACTION_DOWN事件中初始化VelocityTracker对象,比如:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
            VelocityTracker mVelocityTracker =VelocityTracker.obtain();
            //收集速率追踪点数据  
            velocityTracker.addMovement(event);  
            switch (event.getAction()) {
                    ...
                case MotionEvent.ACTION_DOWN:    
                     break;
                    ...
            }
    }

在ACTION_MOVE事件中我们就可以获取当前事件的滑动速率了,关于computeCurrentVelocity(int units,int maxVelocity)

  • units : 我们想要指定的得到的速度单位,如果值为1,代表1毫秒运动了多少像素。如果值为1000,代表1秒内运动了多少像素。如果值为100,代表100毫秒内运动了多少像素。(这个参数设置真有点…….什么鬼嘛!)这个方法还有一个重载函数 computeCurrentVelocity (int units, float maxVelocity), 跟上面一样也就是多了一个参数。

  • maxVelocity : 该方法所能得到的最大速度,这个速度必须和你指定的units使用同样的单位,而且必须是整数.也就是,你指定一个速度的最大值,如果计算超过这个最大值,就使用这个最大值,否则,使用计算的的结果,

    注:最后一定别忘了在ACTION_UP或者ACTION_CANCEL中回收一下

//获取最大速度
mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity()

switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
         //计算瞬时速度  
         mVelocityTracker .computeCurrentVelocity(units, mMaxVelocity);  
         float velocityX = mVelocityTracker .getXVelocity();  
         float velocityY = mVelocityTracker .getYVelocity();   
         if (mVelocityTracker != null) {  
              mVelocityTracker.clear();  
              mVelocityTracker.recycle();  
              mVelocityTracker = null;  
         }    
         break;
        ...
}

LinearLayout控制WebView滚动

实现View简单的移动

注意,是移动,是移动(难不成还是联通- -!)而不是滚动。达到这种效果只需要View的scrollTo或者scrollBy即可实现,不是很复杂。直接看代码把:

package com.tianzhao.demo;

import android.content.Context;
import android.graphics.Color;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.Locale;


public class CustomView extends LinearLayout {

    public CustomView(@NonNull Context context) {
        super(context);
        init(context);
    }

    public CustomView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CustomView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        setOrientation(VERTICAL);
        initChildView(context);
    }

    private void initChildView(Context context) {
        for (int i = 0; i < 60; i++) {
            TextView view = new TextView(context);
            LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,120);
            view.setLayoutParams(params);
            view.setText(String.format(Locale.CHINA,"%d",i));
            view.setGravity(Gravity.CENTER);
            if (i % 2 == 1) {
                view.setBackgroundColor(Color.parseColor("#FF40FF9C"));
            } else {
                view.setBackgroundColor(Color.parseColor("#303F9F"));
            }
            addView(view);
        }
    }

    private float mLastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int offsetY = (int) (event.getY() - mLastY);
                ((View) getParent()).scrollBy(0, -offsetY);
                break;
        }
        return true;
    }

}

来看看效果图,只要手指停下。就不移动了,达不到View滚动的效果。
这里写图片描述

实现View的滚动

注意,这里说的是滚动、是滚动、滚动、动。不是移动!

我们在 onInterceptTouchEvent() 方法中主要处理事件的一些初始化,和是否拦截当前事件的相关操作。而 onTouchEvent() 则主要处理事件的滑动与滚动。不必要的代码我已经剔除了。只留了最基本有用的代码,注释也补充了。雅酷,伊利卡通,代码,开!

package com.tianzhao.demo;

import android.content.Context;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.webkit.WebView;
import android.widget.LinearLayout;
import android.widget.Scroller;

/**
 * Desc:使用LinearLayout的事件来操控WebView的滚动
 * Created by tianzhao on 2017/8/31.
 */
public class CustomView extends LinearLayout {

    private static final String TAG = "CustomView";
    private static final boolean DEBUG_LOG = true;

    private Scroller mScroll;

    private VelocityTracker mVelocityTracker;
    private int mTouchSlop;
    private int mMinimumVelocity;
    private int mMaximumVelocity;

    private WebView mWebView;

    public CustomView(@NonNull Context context) {
        super(context);
        init(context);
    }

    public CustomView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CustomView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {

        mScroll = new Scroller(context);

        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mTouchSlop = viewConfiguration.getScaledTouchSlop();
        mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();

        initChildView(context);
    }

    private void initChildView(Context context) {

        mWebView = new WebView(context);
        mWebView.loadUrl("file:///android_asset/index.html");
        mWebView.setFocusable(false);
        addView(mWebView);
    }

    /**
     * 初始化值,并且选择是否拦截事件
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        boolean isIntercept = false;

        /*
         * 这里不能使用event.getAction()
         * 注意event.getAction()和event.getActionMasked()的区别
         * 当MotionEvent对象只包含一个触摸点的事件时,上边两个函数的结果是相同的,
         * 但是当包含多个触摸点时,二者的结果就不同啦。
         * getAction获得的int值是由pointer的index值和事件类型值组合而成的,
         * 而getActionWithMasked则只返回事件的类型值
         */
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                boolean flingFinished = stopFling();
                mTouchLastY = event.getY();
                mPointerID = event.getPointerId(0);
                if (!flingFinished) {
                    return true;
                } else {
                    addVelocityTracker(event);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int pointerIndex = event.findPointerIndex(mPointerID);
                if (pointerIndex < 0) {
                    break;
                }
                int offsetY = (int) (mTouchLastY - event.getY(pointerIndex));
                mTouchLastY = event.getY();
                //判断上下滑动距离是否大于最小滑动距离
                //如果大于,则将事件向下传递给onTouchEvent处理
                //反之则进行拦截,不进行任何响应
                if (Math.abs(offsetY) >= mTouchSlop) {
                    isIntercept = true;
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                isIntercept = true;
                break;
        }

        return isIntercept;
    }

    private float mTouchLastY;
    private int mPointerID;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        addVelocityTracker(event);
        //这里不能使用event.getAction()
        //注意event.getAction()和event.getActionMasked()的区别
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                //将当前LinearLayout的滑动事件透传给WebView去进行滚动
                int pointerIndex = event.findPointerIndex(mPointerID);
                int offsetY = (int) (mTouchLastY - event.getY(pointerIndex));
                mTouchLastY = event.getY(pointerIndex);
                mWebView.scrollBy(0, offsetY);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //手指离开时,计算当前的滑动速率
                //使用Scroll和VelocityTracker进行滚动
                startFling(-(int) getScrollVelocityY());
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //当两个触摸点,有一个离开屏幕时
                //将当前的触摸点
                solvePointerUp(event);
                break;
        }
        return true;
    }

    private void solvePointerUp(MotionEvent event) {
        // 获取离开屏幕的手指的索引
        int pointerIndexLeave = event.getActionIndex();
        int pointerIdLeave = event.getPointerId(pointerIndexLeave);
        // 离开屏幕的手指如果正是目前的有效手指,此处需要重新调整
        if (mPointerID == pointerIdLeave) {
            // 将还在屏幕的手指标记为有效手指,并且重置VelocityTracker
            int reIndex = pointerIndexLeave == 0 ? 1 : 0;
            mPointerID = event.getPointerId(reIndex);
            // 调整触摸位置,防止出现跳动
            mTouchLastY = event.getY(reIndex);
            // 清除mVelocityTracker记录的信息
            clearVelocity();
        }
    }

    /**
     * VelocityTracker也有最大值和最小值
     * computeCurrentVelocity获取到的只 <= mMaximumVelocity
     * 所以需要判断是否大于最小值
     */
    private float getScrollVelocityY() {
        float velocityY = 0;
        if (mVelocityTracker != null) {
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            velocityY = mVelocityTracker.getYVelocity(mPointerID);
            print("getScrollVelocityY : " + velocityY);
        }
        if (Math.abs(velocityY) > mMinimumVelocity) {
            return velocityY;
        }
        return 0;
    }

    private int mLastFlingY;

    private void startFling(int velocityY) {
        mLastFlingY = 0;
        print(" startFling : " + velocityY);
        mScroll.fling(0, 0, 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
        //注意此处一定要调用postInvalidate()或者invalidate(),置于两者的区别,这块就不多做解释了
        postInvalidate();
    }

    private boolean stopFling() {
        if (mScroll != null && !mScroll.isFinished()) {
            mScroll.abortAnimation();//停止滚动
            return false;
        }
        return true;
    }


    @Override
    public void computeScroll() {
        //此处一定要通过 mScroll.computeScrollOffset() 方法判断是否滚动结束
        //要不然发生死循环就GG了
        if (mScroll != null && mScroll.computeScrollOffset()) {
            int y = mScroll.getCurrY();
            print(" offsetY : " + (y - mLastFlingY));
            mWebView.scrollBy(0, y - mLastFlingY);
            mLastFlingY = y;
            //注意此处同上一样一定要调用postInvalidate()或者invalidate()
            //以达成一种循环的效果
            postInvalidate();
        }
    }

    private void addVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private void clearVelocity() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
    }

    private void print(String msg) {
        if (DEBUG_LOG) {
            Log.d(TAG, msg);
        }
    }
}

这次来看一下最终的效果

这里写图片描述

这节就说道这里把,下节来正式进入主题——WebView与ListView共存。