0
点赞
收藏
分享

微信扫一扫

自定义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);
}
}
}

2、重写onLayout()方法,获取所有的子View,各自调用layout()方法,按下图排列方式,确定它们各自的摆放位置。

首先来认识一下图片的位置:

自定义ViewGroup—实现自定义ViewPager_android

@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);
}
}

3、创建手势识别器Gesturedetector,来完成滑动子视图的功能。

(1) 创建一个手势识别器Gesturedetector
手势识别器通过MotionEvent可以识别出多种手势和事件。当某个特定动作事件发生时,手势识别器Gesturedetector的onTouchEvent(MotionEvent)就会被调用,此方法里再通过调用OnGestureListener定义的回调方法会通知用户具体是什么动作事件。

= 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;
}
});

(2)在WonViewPager里重写onTouchEvent()方法,并将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
mGestureDetector.onTouchEvent(event);
return true;
}

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>

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);
}
}

效果:

自定义ViewGroup—实现自定义ViewPager_android_02


上面实现了通过手指滑动图片的功能。

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;
}

效果:

自定义ViewGroup—实现自定义ViewPager_android_03

  • 平滑的移动到指定位置
    scrollTo(position*getWidth(),0)会直接移动到指定位置,给人一种“突然”的感觉,没有平滑的过渡。我们可以使用Scroller类的startScroll(int startX, int startY, int dx, int dy) 方法来实现View的平滑滚动。

第一步:定义Scroller对象

private Scroller scroller = new Scroller(getContext());

第二步:调用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;
}

第三步:重写computeScroll(),完成实际的滚动

@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),0);
postInvalidate();
}
}

修改的代码:

/*记录当前视图的序号*/
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();
}
}

效果:

自定义ViewGroup—实现自定义ViewPager_自定义ViewPager_04


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;
}

效果:

自定义ViewGroup—实现自定义ViewPager_自定义_05


关于自定义ViewPager就这么多啦,谢谢围观!

具体代码请参考:​​demo​​


举报

相关推荐

0 条评论