自定义ViewGroup—实现自定义ViewPager
ViewGroup和View
1、 ViewGroup是一个可以容纳View的容器,负责测量子视图或子控件的宽和高;并决定子视图或子控件的位置。常用的方法有:
- onMesure():测量子视图或子控件的宽高,以及设置自己的宽和高。
- onLayout():通过getChildCount()获取子view数量,getChildAt获取所有子View,分别调用layout(int l, int t, int r, int b)确定每个子View的摆放位置。
- onSizeChanged():在onMeasure()后执行,只有大小发生了变化才会执行onSizeChange。
- onDraw():默认不会触发,需要手动触发。
2、View根据测量模式和ViewGroup给出的建议宽和高,在ViewGroup为其指定的区域内绘制出自己的形态。常用的方法有:
- onMesure():测试视图大小,主要是处理wrap_content这种情况;
- onDraw():在父视图指定的区域绘制图形。
自定义ViewPager
我们来实现一个轮播图片的自定义ViewPager。
1、继承ViewGroup,并写个添加图片数据的方法,方便添加图片到ViewGroup容器里
package com.wong.support;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ImageView;
import java.util.List;
public class WonViewPager extends ViewGroup { /*要轮翻播放的图片*/ private List<Integer> images; public WonViewPager(Context context) { super(context); } public WonViewPager(Context context, AttributeSet attrs) { super(context, attrs); } public WonViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public WonViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } /*批量设置轮播图片*/ public void setImages(List<Integer> images) { this.images = images; updateViews(); } /*将子视图添加到ViewGroup容器中*/ private void updateViews(){ for(int i = 0; i < images.size(); i++){ ImageView iv = new ImageView(getContext()); iv.setBackgroundResource(images.get(i)); this.addView(iv); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
2、重写onLayout()方法,获取所有的子View,各自调用layout()方法,按下图排列方式,确定它们各自的摆放位置。
首先来认识一下图片的位置:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); for(int i = 0; i < childCount; i++){ View childView = getChildAt(i); childView.layout(i*getWidth(),t,(i+1)*getWidth(),b); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
3、创建手势识别器Gesturedetector,来完成滑动子视图的功能。
(1) 创建一个手势识别器Gesturedetector
手势识别器通过MotionEvent可以识别出多种手势和事件。当某个特定动作事件发生时,手势识别器Gesturedetector的onTouchEvent(MotionEvent)就会被调用,此方法里再通过调用OnGestureListener定义的回调方法会通知用户具体是什么动作事件。
GestureDetector mGestureDetector = new GestureDetector(getContext(),new 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) { //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离 scrollBy((int) distanceX, 0); return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } });
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
(2)在WonViewPager里重写onTouchEvent()方法,并将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。
@Override public boolean onTouchEvent(MotionEvent event) { //将触摸事件传递手势识别器 mGestureDetector.onTouchEvent(event); return true; }
- 1
- 2
- 3
- 4
- 5
- 6
2、应用
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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.wong.support.WonViewPager android:id="@+id/wvp" android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
MainActivity.java
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WonViewPager wonViewPager = findViewById(R.id.wvp); List<Integer> list = new ArrayList<>(); list.add(R.drawable.a); list.add(R.drawable.b); list.add(R.drawable.c); list.add(R.drawable.d); wonViewPager.setImages(list); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
效果:
上面实现了通过手指滑动图片的功能。
4、优化:边界情况的处理和平滑的移动到指定位置。
- 边界情况的处理:当手指松开时,如果滑动偏移的距离超出图片1/2时,自动切换到下个图片,否则回弹到初始位置。这里我们需要在onTouchEvent()中处理触摸事件:
/*记录当前视图的序号*/ private int position; @Override public boolean onTouchEvent(MotionEvent event) { //将触摸事件传递手势识别器 mGestureDetector.onTouchEvent(event); switch (event.getAction()){ /*按下*/ case MotionEvent.ACTION_DOWN: break; /*移动,在ACTION_DOWN和ACTION_UP之间*/ case MotionEvent.ACTION_MOVE: /*返回视图正在展示部分的左边滚动位置(即返回滚动的视图的左边位置)*/ int scrollX = getScrollX(); /*加上父视图的一半*/ int totalWidth = scrollX + getWidth()/2; /*计算视图划过一半后的下一个视图的序号*/ position = totalWidth / getWidth(); /*计算视图划过一半后的下一个视图的序号*/ position = totalWidth / getWidth(); /* scrollX >= getWidth() * (images.size() - 1)说明是最后一张,那么我们就不能让其出界,否则它是可以滑出界的*/ if (scrollX >= getWidth() * (images.size() - 1)) { position = images.size() - 1; } /*scrollX < 0说明左边滑入界了,即第一张视图的左边偏右,距离父视图左边之间的距离出现空白*/ if (scrollX <= 0) { position = 0; } break; /*抬起手指*/ case MotionEvent.ACTION_UP: /*滑动到指定的视图*/ scrollTo(position*getWidth(),0); break; } return true; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
效果:
- 平滑的移动到指定位置
scrollTo(position*getWidth(),0)会直接移动到指定位置,给人一种“突然”的感觉,没有平滑的过渡。我们可以使用Scroller类的startScroll(int startX, int startY, int dx, int dy) 方法来实现View的平滑滚动。
第一步:定义Scroller对象
private Scroller scroller = new Scroller(getContext());
- 1
第二步:调用startScroll(int startX, int startY, int dx, int dy)方法,此方法并不会触发滚动,因为它最终调了以下这个方法(来自android源码),而这个方法只是在收集过程数据而已,调用invalidate()方法触发视图刷新:
/** * Start scrolling by providing a starting point, the distance to travel, * and the duration of the scroll. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. * @param duration Duration of the scroll in milliseconds. */ public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
第三步:重写computeScroll(),完成实际的滚动
@Override public void computeScroll() { super.computeScroll(); if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),0); postInvalidate(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
修改的代码:
/*记录当前视图的序号*/ private int position; private Scroller scroller = new Scroller(getContext()); private int scrollX; @Override public boolean onTouchEvent(MotionEvent event) { //将触摸事件传递手势识别器 mGestureDetector.onTouchEvent(event); switch (event.getAction()){ /*按下*/ case MotionEvent.ACTION_DOWN: break; /*移动,在ACTION_DOWN和ACTION_UP之间*/ case MotionEvent.ACTION_MOVE: /*返回视图正在展示部分的左边滚动位置(即返回滚动的视图的左边位置)*/ scrollX = getScrollX(); /*加上父视图的一半*/ int totalWidth = scrollX + getWidth()/2; /*计算视图划过一半后的下一个视图的序号*/ position = totalWidth / getWidth(); /*计算视图划过一半后的下一个视图的序号*/ position = totalWidth / getWidth(); /* scrollX >= getWidth() * (images.size() - 1)说明是最后一张,那么我们就不能让其出界,否则它是可以滑出界的*/ if (scrollX >= getWidth() * (images.size() - 1)) { position = images.size() - 1; } /*scrollX < 0说明左边滑入界了,即第一张视图的左边偏右,距离父视图左边之间的距离出现空白*/ if (scrollX <= 0) { position = 0; } break; /*抬起手指*/ case MotionEvent.ACTION_UP: /*滑动到指定位置*/
// scrollTo(position*getWidth(),0); /*平滑移动到指定位置*/ scroller.startScroll(scrollX,0,-(scrollX-position*getWidth()),0); /*从UI线程触发视图更新*/ invalidate(); break; } return true; } /** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */ @Override public void computeScroll() { super.computeScroll(); if(scroller.computeScrollOffset()){ /** * 每次x轴有变化都会移动一点,那么要持续变化完,就要调用postInvalidate()持续刷新视图, * 而上面的invalidate()方法只负责第一次触发computeScroll()调用,剩下的都是postInvalidate()触发的 */ scrollTo(scroller.getCurrX(),0); /*从非UI线程触发视图更新,只有调用*/ postInvalidate(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
效果:
5、优化:滑至最后一屏禁止向右滑,滑至第一屏禁止向左滑
在我们前面的例子里,都会发现第一屏向右滑,就出现空白,最后一屏也出现类似的情况。因为我们一开始是在手势识别器做的移动,所以我们可以在手势识别器GestureDetector做文章。
思路:
1、通过手指划过的路径的终点和起点相减,根据正负判断方向;
2、如果是正,则说明向右划,接着判断是不是第一屏,是的话就不滚动;
3、如果是负,则说明向左划,接着判断是不是最一屏,是的话就不滚动;
修改后的GestureDetector代码如下:
GestureDetector mGestureDetector = new GestureDetector(getContext(), new 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) { int startX = 0; switch (e1.getAction()) { case MotionEvent.ACTION_DOWN: startX = (int) e1.getX(); break; } boolean noScroll = false; switch (e2.getAction()) { case MotionEvent.ACTION_MOVE: int endX = (int) e2.getX(); int dx = endX - startX; if (dx < 0) { if (scrollX >= getWidth() * (images.size() - 1)) { noScroll = true; } } if (dx > 0) { if (scrollX <= 0) { noScroll = true; } } break; default: break; } if(!noScroll) { scrollBy((int) distanceX, 0); } return false; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
效果:
关于自定义ViewPager就这么多啦,谢谢围观!
具体代码请参考:demo
文章来源: blog.csdn.net,作者:WongKyunban,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_40763897/article/details/104052120
- 点赞
- 收藏
- 关注作者
评论(0)