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共存。