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

来源:互联网 发布:qq邮箱软件下载 编辑:程序博客网 时间:2024/05/29 06:50

上一篇我们大致了解了一下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共存。

阅读全文
0 0
原创粉丝点击