自定义控件实现ViewPager的效果
1. 新建一个类继承ViewGroup,实现构造方法和onLayout方法
public class MyViewPager extends ViewGroup {
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
2. 添加View到这个ViewGroup中
public class MainActivity extends AppCompatActivity {
private MyViewPager mViewPager;
private int[] ids = {
R.drawable.a1,
R.drawable.a2,
R.drawable.a3,
R.drawable.a4,
R.drawable.a5,
R.drawable.a6
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewPager = (MyViewPager) findViewById(R.id.viewpager);
for (int i = 0; i < ids.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(ids[i]);
mViewPager.addView(imageView);
}
}
}
onLayout(boolean changed, int l, int t, int r, int b)坐标位置分析
以左上角为原点,分别是距离左边的距离,距离上边的距离,距离右边的距离,距离上边的距离。
每个左边表示:(到Y轴的距离,到X轴的距离)
左上角和右下角两个点确定一个View的位置,
公式:左上角(igetWidth,0)
右下角((i+1) * getWidth,getHeight)
view.layout(l,t,r,b)
就是说
l = igetWidth
t = 0
r = (i+1) * getWidth
t = getHeight
遍历每个孩子,给每个孩子指定在屏幕上的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//遍历每个孩子,给每个孩子指定在屏幕上的位置
// l = i * getWidth
// t = 0
// r = (i+1) * getWidth
// t = getHeight
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.layout(i * getWidth(),0,(i + 1) * getWidth(),getHeight());
}
}
这样就可以显示第一张图片了
代码:代码
3. 给ViewPager添加手势识别器
手势识别器的使用步骤
* 1,定义出来
* 2,实例化--重写想要的方法
* 3,在onTouchEvent()中把事件传递给手势识别器
/**
* 手势识别器
* 1,定义出来
* 2,实例化--重写想要的方法
* 3,在onTouchEvent()中把事件传递给手势识别器
*
*/
private GestureDetector mDetector;
/**
* 构造方法
* @param context
* @param attrs
*/
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
//实例化手势识别器
mDetector = new GestureDetector(context,new MyGestureListener());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//3,把事件传递给手势识别器
mDetector.onTouchEvent(event);
return true;//自己处理
}
class MyGestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
在onScroll方法中,设置在X轴和Y轴都能滑动:
scrollBy((int) distanceX, (int) distanceY);
scrollBy和scrollTo的区别
两者 移动的都是view的内容 view本身 是不移动的 所以getx gety 一直是不变的
scrollTo :相对于初始位置移动
scrollBy : 相对于上次移动的最后位置移动
源码:
/**
* 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);
}
/**
* 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();
}
}
}
1)scrollTo()是一步到位;
2)scrollBy()是逐步累加
1)假设有一个View,如果想把SView从(0, 0)移动到(100, 100)。注意,这里说的(0, 0)和(100, 100),指的是SView左上角的坐标。那么偏移量就是原点(0, 0)到目标点(100, 100)的距离,即(0 , 0) - (100, 100) = (-100, -100)。
只需要调用SView.scrollTo(-100, -100)就可以了。scrollTo(int x, int y)的两个参数x和y,代表的是偏移量,这时的参照物是(0, 0)点。
然而,scrollBy()是有一定的区别的。scrollBy()的参照物是(0, 0)点加上偏移量之后的坐标。
2)假设SView调用了scrollTo(-100, -100),此时SView左上角的坐标是(100, 100),这时再调用scrollBy(-20, -20),此时SView的左上角就被绘制到了(120, 120)这个位置
滑动显示页面的原理
//下标位置
int tempIndex = currentIndex;
if ((startX - endX) > getWidth() / 2) {
//显示下一个页面
tempIndex ++;
} else if ((endX - startX) > getWidth() / 2) {
//显示上一个页面
tempIndex --;
}
滑动显示指定页面
private float startX;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//3,把事件传递给手势识别器
mDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录坐标
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
//结束坐标
float endX = event.getX();
//下标位置
int tempIndex = currentIndex;
if ((startX - endX) > getWidth() / 2) {
//显示下一个页面
tempIndex ++;
} else if ((endX - startX) > getWidth() / 2) {
//显示上一个页面
tempIndex --;
}
//根据下标位置移动到指定页面
scrollToPager(tempIndex);
break;
default:
break;
}
return true;//自己处理
}
/**
* 根据下标位置移动到指定页面 过滤非法值
* @param tempIndex
*/
private void scrollToPager(int tempIndex) {
if (tempIndex < 0) {
tempIndex = 0;
}
if (tempIndex > getChildCount() - 1) {
tempIndex = getChildCount() - 1;
}
//把过滤后的值赋值给当前页面下标
currentIndex = tempIndex;
scrollTo(currentIndex * getWidth(),0);
}
解决生硬回弹问题的原理
把一次移动改为多次移动
自定义滑动帮助类MyScroller
package com.zhang.custom_viewpager;
import android.os.SystemClock;
class MyScroller {
/**
* X轴起始坐标
*/
private float startX;
/**
* Y轴起始坐标
*/
private float startY;
/**
* 在X轴移动的距离
*/
private int distanceX;
/**
* 在Y轴移动的距离
*/
private int distanceY;
/**
* 开始时间
*/
private long startTime;
/**
* 是否 移动完成
* FALSE没有移动完成
* TRUE移动完成
*/
private boolean isFinish;
/**
* 总时间 写死
*/
private long totalTime = 500;
private float currX;
public float getCurrX() {
return currX;
}
void startScroll(float startX, float startY, int distanceX, int distanceY) {
this.startX = startX;
this.startY = startY;
this.distanceX = distanceX;
this.distanceY = distanceY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}
/**
* 速度
* 求移动一小段的距离
* 求移动一小段对应的坐标
* 求移动一小段对应的时间
* true:正在移动
* false:移动结束
*
* @return
*/
boolean computeScrollOffset() {
if (isFinish) {
return false;
}
long endTime = SystemClock.uptimeMillis();
//一小段花的时间
long passTime = endTime - startTime;
if (passTime < totalTime) {
//移动还没有结束
//计算平均速度
// float v = distanceX / totalTime;
//移动这个一小段对应的距离
float distanceSmallX = passTime * distanceX / totalTime;
currX = startX + distanceSmallX;
} else {
//移动结束
isFinish = true;
currX = startX + distanceX;
}
return true;
}
}
public class MyViewPager extends ViewGroup {
/**
* 手势识别器
* 1,定义出来
* 2,实例化--重写想要的方法
* 3,在onTouchEvent()中把事件传递给手势识别器
*/
private GestureDetector mDetector;
/**
* 当前页面下标位置
*/
private int currentIndex;
private MyScroller mScroller;//自己写的
// private Scroller mScroller;//系统的
/**
* 构造方法
*
* @param context
* @param attrs
*/
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
mScroller = new MyScroller();
// mScroller = new Scroller(context);
//实例化手势识别器
mDetector = new GestureDetector(context, new MyGestureListener());
}
private float startX;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//3,把事件传递给手势识别器
mDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录坐标
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
//结束坐标
float endX = event.getX();
//下标位置
int tempIndex = currentIndex;
if ((startX - endX) > getWidth() / 2) {
//显示下一个页面
tempIndex ++;
} else if ((endX - startX) > getWidth() / 2) {
//显示上一个页面
tempIndex --;
}
//根据下标位置移动到指定页面
scrollToPager(tempIndex);
break;
default:
break;
}
return true;//自己处理
}
/**
* 根据下标位置移动到指定页面 过滤非法值
* @param tempIndex
*/
private void scrollToPager(int tempIndex) {
if (tempIndex < 0) {
tempIndex = 0;
}
if (tempIndex > getChildCount() - 1) {
tempIndex = getChildCount() - 1;
}
//把过滤后的值赋值给当前页面下标
currentIndex = tempIndex;
// scrollTo(currentIndex * getWidth(),0);
int distanceX = currentIndex*getWidth() - getScrollX();
mScroller.startScroll(getScrollX(),getScrollY(),distanceX,0);
invalidate();//这个方法会导致onDraw()和computeScroll()执行
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
float currentX = mScroller.getCurrX();
scrollTo((int) currentX,0);
invalidate();
}
}
class MyGestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/**
* @param e1
* @param e2
* @param distanceX:在X轴滑动的距离
* @param distanceY:在Y轴滑动的距离
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// scrollBy((int) distanceX, (int) distanceY);//这样可以同时在X轴和Y轴滑动
scrollBy((int) distanceX, 0);//只能在X轴滑动
return true;//自己处理滑动事件
}
@Override
public void onLongPress(MotionEvent e) {
showLog("长按");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
private void showLog(String msg) {
Log.e("===z", msg);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//遍历每个孩子,给每个孩子指定在屏幕上的位置
// l = i * getWidth
// t = 0
// r = (i+1) * getWidth
// t = getHeight
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
}
最后可以把自定义的Scroller改成系统的Scroller
mScroller = new Scroller(context);
限制第一页和最后一页不能滑动
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// scrollBy((int) distanceX, (int) distanceY);//这样可以同时在X轴和Y轴滑动
//限制第一页和最后一页不能滑动
if (currentIndex == 0 || currentIndex == getChildCount() - 1) {
return false;
}
scrollBy((int) distanceX, 0);//只能在X轴滑动
return true;//自己处理滑动事件
}
代码:代码
添加测试页面:
//添加测试页面
View view = View.inflate(this, R.layout.test, null);
mViewPager.addView(view,2);
其他页面能显示 是因为在onLayout中强制设置一级子View的固定大小,并没有考虑它想要多大,自身多大。第三个添加的测试页面其实也显示了。只是一级子View下面的View没有被测量,所以没有显示。
获取自定义的ViewGroup中的子View并测量,就可以显示了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取子View并测量
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(widthMeasureSpec,heightMeasureSpec);
}
}
测量的过程
onMeasure()方法解析:
* @param widthMeasureSpec : 包含两个属性:父类建议的宽和测量模式
* @param heightMeasureSpec :父类建议的高和测量模式
测量模式有这三种
MeasureSpec.AT_MOST; :最大值模式,当控件宽高设置为wrap_content时
MeasureSpec.EXACTLY :精确值模式,当控件match_parent时
MeasureSpec.UNSPECIFIED; :未指定模式,View想多大就多大,通常在绘制自定义View时才会用
获取下一级子View的建议宽和测量模式
MeasureSpec.makeMeasureSpec(size, mode);
/**
*
* @param widthMeasureSpec : 包含两个属性:父类建议的宽和测量模式
* @param heightMeasureSpec :父类建议的高和测量模式
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
/**
* 测量模式有这三种
* MeasureSpec.AT_MOST; :最大值模式,当控件宽高设置为wrap_content时
* MeasureSpec.EXACTLY :精确值模式,当控件match_parent时
* MeasureSpec.UNSPECIFIED; :未指定模式,View想多大就多大,通常在绘制自定义View时才会用
*/
//获取下一级子View的建议宽和测量模式
int makeMeasureSpec = MeasureSpec.makeMeasureSpec(size, mode);
//获取子View并测量
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(widthMeasureSpec,heightMeasureSpec);
}
}
事件冲突
在第三个页面滑动ScrollView部分时,没有反应。
触摸事件的传递过程:
事件 -》Activity -》传递给MyViewPager -》传递给子View
子View没有处理
事件再回传给MyViewPager,MyViewPager的touchEvent返回TRUE,消费了该事件。
当事件传递给ScrollView时,ScrollView消费了该事件,事件结束,所以不会回传到MyViewPager,所以滑动ScrollView时没有反应了。
该冲突的解决方案:
在ScrollView左右滑动时,MyViewPager拦截事件自己消费,ScrollView上下滑动时,MyViewPager不拦截事件继续传递
private float eStartX;
private float eStartY;
/**
* 事件拦截方法
* 返回true,拦截事件,将会触发当前控件的onTouchEvent()方法
* 返回false,不拦截事件,事件继续传递给子View
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//把事件传递给手势识别器
mDetector.onTouchEvent(ev);
boolean result = false;//默认传递事件
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
eStartX = ev.getX();
eStartY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float eEndX = ev.getX();
float eEndY = ev.getY();
float distanceX = Math.abs((eEndX - eStartX));
float distanceY = Math.abs((eEndY - eStartY));
//如果X轴滑动的距离大于Y轴滑动的距离,则拦截事件
if (distanceX > distanceY && distanceX > 10) {
result = true;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return result;
}
代码:解决事件冲突