自定义ViewGroup—实现自定义ViewPager

举报
yd_221104950 发表于 2020/12/03 00:35:13 2020/12/03
【摘要】 ViewGroup和View 1、 ViewGroup是一个可以容纳View的容器,负责测量子视图或子控件的宽和高;并决定子视图或子控件的位置。常用的方法有: onMesure():测量子视图或子控件的宽高,以及设置自己的宽和高。onLayout():通过getChildCount()获取子view数量,getChildAt获取所有子View,分别调用layout(...

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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。