1.scrollBy()和scrollTo()的区别:
scrollBy(x, y)
源码:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
可以看到scrollBy()方法中就是调用了scrollTo()方法,但是传递给scrollTo()方法的参数是在当前View的偏移量的基础上增加了x,y。
这里的mScrollX,mScrollY 是偏移量,是相对自己初始位置的滑动偏移距离,只有当有scroll事件发生时,这两个方法才能有值。
所谓自己初始位置是指,控件在刚开始显示时、没有滑动前的位置。
可以看下这两个方法执行的区别
scrollBy(-30, -50)
scrollTo(-30, -50)
调用scrollBy(-30, -50) ,view就会会发生一段偏移,而scrollTo(-30, -50)view只会偏移一次。
还有一点需要注意:我们想对TextView进行移动,必须调用其父布局的scrollBy()或者scrollTo()方法,因为这两个方法是对View的内容进行偏移,并不是对View本身进行偏移。
2.getX / getY 和 getRawX / getRawY的区别
它们的区别其实很简单,getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
实现自定义ViewPager
public class MyViewPager extends ViewGroup {
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
}
public class MainActivity extends AppCompatActivity {
private MyViewPager myViewPager;
private int[] ids = new int[]{R.mipmap.tu1,R.mipmap.tu2,R.mipmap.tu3};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
myViewPager = findViewById(R.id.my_view_pager);
for (int i = 0; i < ids.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(ids[i]);
myViewPager.addView(imageView);
}
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customviewpager.MyViewPager
android:id="@+id/my_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
重写onLayout()方法,获取所有的子View,各自调用layout()方法,确定它们各自的摆放位置。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
接下来要让让ViewGroup中的元素,跟着手的滑动而滑动了。使用手势识别器Gesturedetector来处理滑动事件。
(1) 创建一个手势识别器:这里主要就是靠 scrollBy()方法,来实现View跟随手的滑动而滑动。这个方法会调用onScrollChanged方法,并刷新视图。
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//当有手指在屏幕上滑动的时候回调
// distanceX为正时,向左移动,为负时,向右移动
//dx表示x方向上移动的距离,dy表示y方向上移动的距离。往坐标轴正方向上移动的话,值就是正值;反之为负
scrollBy((int) distanceX, 0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
(2)重写onTouchEvent(),将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
mGestureDetector.onTouchEvent(event);
return true;
}
现在View已经可以跟随我们的手势滑动了,但离我们预期的效果,还差两个小问题待解决:边界情况的处理和平滑的回弹到指定位置。
(3)边界情况的处理。
我们期望的效果是:手指松开时,当滑动偏移的距离超出图片1/2时,自动切换到下个图片;小于1/2,回弹到初始位置。这里我们需要在onTouchEvent()中处理触摸事件,具体代码实现如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
scrollX = getScrollX();//相对于初始位置滑动的距离
//你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//滑到最后一张的时候,不能出边界
if (position >= getChildCount()) {
position = getChildCount() - 1;
}
if (position < 0) {
position = 0;
}
break;
case MotionEvent.ACTION_UP:
//绝对滑动,直接滑到指定的x,y的位置,较迟钝
scrollTo(position * getWidth(), 0);
break;
}
return true;
}
这里暂时我们使用的scrollTo(int x,int y)这个方法:让它到某个临界值时,滑动到指定位置,由于它是让view直接滚动到参数x和y所标定的坐标,可以看到下面的运行效果很迟钝。
(4) 如何实现平滑的回弹到指定位置呢?这里就要用到Scroller这个类了。
Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。
Scroller mScroller = new Scroller(mContext);
在onTouchEvent()中的up事件中将scrollTo()方法替换为:mScroller.startScroll();
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
scrollX = getScrollX();//相对于初始位置滑动的距离
//你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//滑到最后一张的时候,不能出边界
if (position >= getChildCount()) {
position = getChildCount() - 1;
}
if (position < 0) {
position = 0;
}
break;
case MotionEvent.ACTION_UP:
//绝对滑动,直接滑到指定的x,y的位置,较迟钝
// scrollTo(position * getWidth(), 0);
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
//使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法
invalidate();
break;
}
return true;
}
其实Scroller的原理就是用ScrollTo()来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate(),这样才会一直回调computeScroll()这个方法,直到滑动结束。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidate();
}
}
MyViewPager是左右滑动,子View(ScrollView)是上下滑动。事件传递的过程中,如果父View无拦截无消耗,那么当事件传递到子View时,默认会被子View(ScrollView)消费,那么事件在ScrollView中就传递结束了,所以父View(MyViewPager)的左右滑动就失效了。
解决冲突的办法:
就是重写父View的onInterceptTouchEvent()事件,在合适的时候,拦截该事件。
onInterceptTouchEvent()方法返回值的含义:
1、 如果return true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
2、 如果return false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
根据我们的期望的效果:左右滑动时,让父View消费该事件;上下滑动时,直接放行,让子View(ScrollView)自己处理。代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
originalX = (int) ev.getX();
originalY = (int) ev.getY();
//这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
mGestureDetector.onTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
//上下滑动拦截,左右滑动不拦截
int currentX = (int) ev.getX();
int currentY = (int) ev.getY();
int dx = currentX - originalX;
int dy = currentY - originalY;
if (Math.abs(dx) > Math.abs(dy)) {
// 左右滑动
return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
} else {
return false;
}
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
完整代码:
package com.example.customviewpager;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class MyViewPager extends ViewGroup {
private GestureDetector mGestureDetector;
private int scrollX;
private int position;
private Scroller mScroller;
private int originalX = 0;
private int originalY = 0;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
mScroller = new Scroller(context);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//当有手指在屏幕上滑动的时候回调
// distanceX为正时,向左移动,为负时,向右移动
// 移动屏幕的方法scrollBy,很重要,这个方法会调用onScrollChanged方法,并刷新视图
//dx表示x方向上移动的距离,dy表示y方向上移动的距离。往坐标轴正方向上移动的话,值就是正值;反之为负
// scrollBy内部实际上是重写了scrollTo方法,scrollTo是将当前视图的基准点移动到某个坐标点
scrollBy((int) distanceX, 0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
originalX = (int) ev.getX();
originalY = (int) ev.getY();
//这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
mGestureDetector.onTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
//上下滑动拦截,左右滑动不拦截
int currentX = (int) ev.getX();
int currentY = (int) ev.getY();
int dx = currentX - originalX;
int dy = currentY - originalY;
if (Math.abs(dx) > Math.abs(dy)) {
// 左右滑动
return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
} else {
return false;
}
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
scrollX = getScrollX();//相对于初始位置滑动的距离
//你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
position = (getScrollX() + getWidth() / 2) / getWidth();
//滑到最后一张的时候,不能出边界
if (position >= getChildCount()) {
position = getChildCount() - 1;
}
if (position < 0) {
position = 0;
}
break;
case MotionEvent.ACTION_UP:
//绝对滑动,直接滑到指定的x,y的位置,较迟钝
// scrollTo(position * getWidth(), 0);
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
//使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
}
package com.example.customviewpager;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private MyViewPager myViewPager;
private int[] ids = new int[]{R.mipmap.tu1,R.mipmap.tu2,R.mipmap.tu3};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
myViewPager = findViewById(R.id.my_view_pager);
for (int i = 0; i < ids.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(ids[i]);
myViewPager.addView(imageView);
}
View textView = View.inflate(this,R.layout.scroll_view,null);
myViewPager.addView(textView);
}
}
原创微博https://www.jianshu.com/p/af8e14ff5f0c
demo链接https://download.csdn.net/download/jingerlovexiaojie/12400548