坐标系
原点在左上点 向右X 递增 向下Y 递增 (相对 和 绝对坐标)
getRawX() 获取触摸点到屏幕左边的距离
getRawY() 获取触摸点到屏幕的上边的距离
getX() 获取触摸点到父控件左边的距离
getY() 获取触摸点到父控件上边的距离
也可以使用 getLocationOnScreen(int localtion()) 获取
滑动的七种方法
layout 方法
这个中方法是因为ViewGroup 在布局时调用子控件的layout方法来布局
public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
public ScrollTextView(Context context) {
super(context);
}
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
int lastX;
int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
offsetLeftAndRight 与offsetTopAndBottom
与layout类似
public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
public ScrollTextView(Context context) {
super(context);
}
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
int lastX;
int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
//layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
LayoutParms
通过修改子view的layout参数 来实现scroll 会对别的子view 有一定的影响
public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
public ScrollTextView(Context context) {
super(context);
}
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
int lastX;
int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
scrollTo 与 scrollBy
scrollTo(绝对的位置) 与 scrollBy(相对的位置) 是对当前控件的内容的滚动
scrollTo 移动到哪就到哪
scrollBy 在当前位置上再移动多少
但方向与你计算的刚好是反的
public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
public ScrollTextView(Context context) {
super(context);
}
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
int lastX;
int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
View pv = (View) getParent();
pv.scrollBy(-offsetX,-offsetY);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
Scoller 类
scoller 用于计算移动的数值 但还是需要 scrollTo 与 scrollBy 来移动view 让移动更平滑
public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
public ScrollTextView(Context context) {
super(context);
init(context);
}
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
Scroller mScroller;
void init(Context context) {
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
//判断Scroller 是否执行完毕
if (mScroller.computeScrollOffset()) {
View pv = (View) getParent();
pv.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
int lastX;
int lastY;
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE: {
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
View pv = (View) getParent();
pv.scrollBy(-offsetX, -offsetY);
lastX = rawX;
lastY = rawY;
}
break;
case MotionEvent.ACTION_UP: {
View pv = (View) getParent();
mScroller.startScroll(pv.getScrollX(), pv.getScrollY(), -pv.getScrollX(), -pv.getScrollY());
invalidate();
}
break;
}
return true;
}
}
属性动画
textView.animate().translationX(100).setDuration(100).start();
大boss ViewDragHelper
- 初始化 ViewDragHelper.create(this,callback)
- 拦截事件 自定义ViewGroup中的拦截事件交给它
- 处理computScroll()
- 处理回调Callback
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
public class DragViewGroup extends FrameLayout {
public DragViewGroup( Context context) {
super(context);
init();
}
public DragViewGroup( Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DragViewGroup( Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
ViewDragHelper mViewDragHelper;
View mMenuView;
View mMainView;
int mWidth;
void init(){
mViewDragHelper = ViewDragHelper.create(this,callback);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//交给ViewDragHelper 来处理
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//交给ViewDragHelper 来处理
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
ViewDragHelper.Callback callback = new ViewDragHelper.Callback(){
//何时开始检测触摸事件
@Override
public boolean tryCaptureView(View view, int i) {
//当前触摸的view 是mMainView 开始检测
return mMainView == view;
}
//处理垂直滑动
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
//处理水平滑动
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
super.clampViewPositionHorizontal(child, left, dx);
return left;
}
//拖动结束后回调
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (mMainView.getLeft()<500){
//关闭菜单
//相当于Scroller 的 startScroll 方法
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else {
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
//用户触摸到view 回调
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
//在拖曳状态改变时回调
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
//位置改变时回调 常用于滑动时更改scale 进行缩放效果
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
};
}
TouchSlop MotionEvent
ActionDown 手指按下
ActionMove 手指移动
ActionUp 手指抬起
TouchSlop:系统所能识别的滑动最小距离 是个常量
int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
VelocityTracker
VelocityTracker 速度追踪 用于追踪手指在滑动过程中的速度 包括水平 和竖直方向的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
@Override
public boolean onTouchEvent(MotionEvent event) {
//添加事件
velocityTracker.addMovement(event);
//当我们想知道当前的滑动数度时可以使用如下方法
//units
velocityTracker.computeCurrentVelocity(1000);
//在1000ms 内 手指在水平方向划过xV 从左到右为正
int xV = (int) velocityTracker.getXVelocity();
//在1000ms 内 手指在竖直方向划过yV 从上到下为正
int yV = (int) velocityTracker.getYVelocity();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//当不需要的时候进行回收
velocityTracker.clear();
velocityTracker.recycle();
}
嵌套滑动滑动冲突
常见的冲突场景
1,外部滑动方向和内部滑动方向不一致
2,外部滑动方向和内部滑动方向一致
3,两种情况的嵌套
真对三种情况如何处理
对于第一中情况: 我们可以判断用户是左右滑动还是上下滑动来进行事件的拦截
第二种情况:我们可以 当滑动到指定的位置后 再有下个view 进行滑动
解决方案:
1,外部拦截法
在onInterceptTouchEvent 中进行拦截
2,内部拦截法
getParent().requestDisallowInterceptTouchEvent(true); 告诉父view 不要拦截让我来消费事件
public class HorizontalScrollViewEx extends ViewGroup {
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
VelocityTracker velocityTracker;
Scroller scroller;
void init(){
velocityTracker = VelocityTracker.obtain();
scroller = new Scroller(getContext());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
boolean intercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)){
intercept = true;
}else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
Log.e("vic-zhang",ev.getAction() + ">>>>intercept="+intercept);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercept;
}
int mLastX,mLastY;
int mLastXIntercept,mLastYIntercept;
int childIdex;
@Override
public boolean onTouchEvent(MotionEvent ev) {
velocityTracker.addMovement(ev);
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX,0);
//Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
velocityTracker.computeCurrentVelocity(1000);
float xV = velocityTracker.getXVelocity();
if (Math.abs(xV)>=50){
childIdex = xV>0?childIdex-1:childIdex+1;
}else {
childIdex= (scrollX+mChildWidth/2)/mChildWidth;
}
childIdex = Math.max(0,Math.min(childIdex,getChildCount()-1));
int dx = mChildWidth*childIdex - scrollX;
scroller.startScroll(scrollX,0,dx,0);
velocityTracker.clear();
Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_UP scrollX="+scrollX);
break;
}
mLastX = x;
mLastY = y;
return true;
//return super.onTouchEvent(ev);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//确定 view group 的大小
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//setMeasuredDimension(getMaxChildWidth(),height);
//我们使用固定的宽度高度 不需要 AT_MOST
setMeasuredDimension(width,height);
//测量子空间的宽度高度
int count = getChildCount();
for (int i = 0; i <count ; i++) {
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
int mChildWidth;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int top = 0;
int width = 0;
for (int i = 0; i <count ; i++) {
View child = getChildAt(i);
int childH = child.getMeasuredHeight();
int w = child.getMeasuredWidth();
mChildWidth = w;
//child.layout(l,top,r,top+childH);
child.layout(width,t,w+width,b);
top = top + childH;
width+=w;
}
}
}
系统提供的嵌套滑动的机制
NestedScrollingParent和NestedScrollingChild
ScrollView 中嵌套 ViewPaer 导致 viewpager 中的内容不显示
如何解决:可以改用 NestedScrollView 和 RecyclerView PagerSnapHelper
NestedScrollingChild child;
NestedScrollingChild2 child2; //增强版 更好的处理Fling
NestedScrollingChildHelper helper;
NestedScrollingParent parent;
NestedScrollingParent2 parent2;//增强版 更好的处理Fling
NestedScrollingParentHelper parentHelper;
示例代码
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.v4.view.NestedScrollingParent2;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class DampLayout extends LinearLayout implements NestedScrollingParent2 {
public DampLayout(Context context) {
super(context);
init();
}
public DampLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DampLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private static final int MAX_HEIGHT = 100;
View headerView;
View footerView;
View childView;
void init(){
headerView = new View(getContext());
footerView = new View(getContext());
}
//在setContentView之后、onMeasure之前调用的方法
@Override
protected void onFinishInflate() {
super.onFinishInflate();
childView = getChildAt(0);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, MAX_HEIGHT);
headerView.setBackgroundColor(Color.parseColor("#00FF00"));
footerView.setBackgroundColor(Color.parseColor("#0000FF"));
addView(headerView,0,layoutParams);
addView(footerView,getChildCount(),layoutParams);
//上移 隐藏 header
scrollBy(0,MAX_HEIGHT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = childView.getLayoutParams();
params.height = getMeasuredHeight();
}
/**
* 当 子 view 调用 startNestedScroll 借助NestedScrollingChildHelper触发
* @param child ViewParent包含触发嵌套滚动的view的对象
* @param target 触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的
* @param nestedScrollAxes 就是嵌套滚动的滚动方向了
* @param type TYPE_TOUCH 标识手指触碰 Scroll TYPE_NON_TOUCH 标识非手指触碰 Fling
* @return
*/
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
//返回true代表处理本次事件
return target instanceof NestedScrollView;
}
/**
* 在回调了 onStartNestedScroll 之后 如果onStartNestedScroll返回true的话,会立即回调
* @param child
* @param target
* @param nestedScrollAxes
* @param type
*/
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
}
/**
* 当 子 view 调用 stopNestedScroll 借助NestedScrollingChildHelper触发
* @param target
* @param type
*/
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
}
/**
* child滑动以后回调的函数
* @param target
* @param dxConsumed 表示target已经消费的x方向的距离
* @param dyConsumed 表示target已经消费的y方向的距离
* @param dxUnconsumed 表示x方向剩下的滑动距离
* @param dyUnconsumed 表示y方向剩下的滑动距离
* @param type
*/
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
getParent().requestDisallowInterceptTouchEvent(true);
if (type == ViewCompat.TYPE_NON_TOUCH) {//非手指触发的滑动,即Filing
// // dy>0向下fling dy<0向上fling
// boolean showTop = dyUnconsumed < 0 && !target.canScrollVertically(-1);
// boolean showBottom = dyUnconsumed > 0 && !target.canScrollVertically(1);
// if (showTop || showBottom) {
// scrollBy(0, dyUnconsumed);
// }
// adjust(dyUnconsumed, target);//调整错位
}
}
/**
* 在子view 滑动前调用
* parent可以选择先于child进行滑动
* @param target
* @param dx 表示target本次滚动产生的x方向的滚动总距离
* @param dy 表示target本次滚动产生的y方向的滚动总距离
* @param consumed 表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
* @param type
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
//告诉 父View 事件交给我来处理
getParent().requestDisallowInterceptTouchEvent(true);
// if (type == ViewCompat.TYPE_TOUCH){
scrollBy(0,dy);
// consumed[1] = dy;
// adjust(dy, target);//调整错位
// }
}
private void adjust(int condition1, View condition2) {
if (condition1 > 0 && getScrollY() > MAX_HEIGHT && !condition2.canScrollVertically(-1)) {
scrollTo(0, MAX_HEIGHT);
}
if (condition1 < 0 && getScrollY() < MAX_HEIGHT && !condition2.canScrollVertically(1)) {
scrollTo(0, MAX_HEIGHT);
}
}
/**
* 限制滑动 移动y轴不能超出最大范围
*/
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
} else if (y > MAX_HEIGHT * 2) {
y = MAX_HEIGHT * 2;
}
super.scrollTo(x, y);
}
}