Android轮播实现
前几天研究了Android的事件分发机制和View的实现流程,然后按照教程实现了轮播图,现在赶紧整理一下写成博客,方便日后参考,防止自己忘记。整个实现过程是一个由浅入深,循序渐进的过程,所以,实现的思路很重要。
首先要明白,轮播图核心是一个自定义ViewGroup,而非自定义View,在此称之为LoopViewGroup,大体的实现思路主要有一下几点:
- LoopViewGroup的绘制过程;
- 利用Scroller实现手动轮播效果
- 利用Timer、TimerTask、Handler实现自动轮播的效果
- 实现轮播图片的点击事件
- 底部轮播圆点的布局及切换过程
LoopViewGroup的绘制过程
首先,要重写onMeasure()方法,在该方法中,根据子视图的个数以及第一个子视图的测量宽高,来设置LoopViewGroup自身的测量宽高,相关代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1、获取子视图的个数
mChildCount = getChildCount();
//2、测量子视图的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);
View view = getChildAt(0);
if (view != null && view.getVisibility() != GONE) {
childMeasureWidth = view.getMeasuredWidth();
childMeasureHeight = view.getMeasuredHeight();
}
//3、设置测量宽高
if (mChildCount == 0) {
setMeasuredDimension(0, 0);
} else {
setMeasuredDimension(childMeasureWidth * mChildCount, childMeasureHeight);
}
}
其次,要重写onLayout()方法,在该方法中,遍历子视图,并设置子视图的位置,子视图是在LoopViewGroup中水平依次平铺的,相关代码:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int marginLeft = 0;
for (int i = 0; i < mChildCount; i++) {
View childView = getChildAt(i);
if (childView != null && childView.getVisibility() != GONE) {
childView.layout(marginLeft, t, marginLeft + childMeasureWidth, b);
marginLeft += childMeasureWidth;
}
}
}
}
由于绘制过程是最简单的自定义ViewGroup的过程,所以此处不作说明。
利用Scroller实现手动轮播效果
要想实现手动轮播,肯定要涉及到事件的分发过程,所以就需要拦截事件和对事件进行处理,也就是重写onInterceptTouchEvent()返回true,并重写onTouchEvent()方法,其中有四个要点需要清楚:
- 我们在滑动图片的过程中,其实就是我们自定义ViewGroup滑动子试图的移动过程,那么只需要知道滑动之前的横坐标和滑动之后的横坐标,此时可以求得此次移动过程的距离,我们再利用scrollBy方法实现图片的滑动,所以,有两个值需要知道,滑动前的横坐标和滑动后的横坐标;
- 我们在第一次按下的一瞬间,此时的移动之前的横坐标和移动之后的横坐标是相等的,也就是我们按下的那一点的横坐标;
- 我们在滑动过程中,是不断的调用ACTION_MOVE方法的,因此我们就应该将移动之前和移动之后的值进行保存,以便我们计算滑动的距离;此时如果是第一张图片,就不允许再向右滑,如果是最后一张图片,就不允许再向左滑;
- 我们在抬起手的一瞬间,需要计算将要滑动到哪个图片上,此时可以求的将要滑动到那张图片的索引值:(当前ViewGroup的滑动位置 + 我们每张图片的宽度 / 2) / 每张图片的宽度;然后就可以利用Scroller滑动到图片上。
相关代码如下:
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) { //如果滑动还未停止,又发生了点击事件,则停止之前的滑动,重新开始
mScroller.abortAnimation();
}
x = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) event.getX();
int distance = moveX - x;
if (distance > 0 && index <= 0) {
} else if (distance < 0 && index >= mChildCount - 1) {
} else {
scrollBy(-distance, 0);
x = moveX;
}
break;
case MotionEvent.ACTION_UP:
int scroll = getScrollX();
index = (scroll + childMeasureWidth / 2) / childMeasureWidth;
if (index < 0) {
index = 0;
} else if (index > mChildCount - 1) {
index = mChildCount - 1;
} else {
int dx = index * childMeasureWidth - scroll; //需要滑动的距离
smoothScrollBy(dx, 0);
}
break;
default:
break;
}
return true;
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, dy, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
要想利用Scroller实现弹性滑动,而非突兀的完成切换过程,需要在抬起手的瞬间启动Scroller,而invalidate()方法会不断的触发computeScroll()方法,从而实现动画切换的效果;当然,在按下屏幕的瞬间,要判断Scroller是否结束,没有结束需要手动结束,以便进入下一次循环,否则会造成事件紊乱。现在,已经可以实现手动轮播效果了。
利用Timer、TimerTask、Handler实现自动轮播的效果
利用Timer,TimerTask,Handler三者实现自动轮播,有以下几个要点:
- 需要两个方法来控制自动轮播的启动和关闭,我们称之为自动轮播的开关,分别为startAuto(),stopAuto();还需要一个标志来表明当前自动轮播的状态时开启还是关闭,设为布尔类型isAuto,true表示自动轮播启动,false表示自动轮播关闭。
- 在LoopViewGroup的构造方法中,设置一个定时任务,如果自动轮播处于开启状态,则利用Handler每间隔一段时间发送一个空的消息,而在Handler接受消息后利用scrollTo()方法实现图片的轮播,相关代码如下:
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
}
timerTask = new TimerTask() {
@Override
public void run() {
if (isAuto) {
autoHandler.sendEmptyMessage(0);
}
}
};
timer.schedule(timerTask, 100, periodTime);
}
private Handler autoHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
if (++index >= mChildCount) { //如果当前图片时最后一张,那么下一张图片是第一张
index = 0;
}
scrollTo(index * childMeasureWidth, 0); //1
break;
}
super.handleMessage(msg);
}
};
在点击发生时,关闭自动轮播,抬起手后需要开启自动轮播,实现过程只需要在onTouchEvent()方法中,当MotionEvent.ACTION_DOWN时,调用stopAuto();当MotionEvent.ACTION_UP时,调用startAuto()即可。
实现轮播图片的点击事件
要想获得点击事件,采用的方法是利用一个变量进行判断,当用户离开屏幕的一瞬间,判断变量开关来判断是点击事件还是移动事件;实现过程如下:
- onTouchEvent()中,当MotionEvent.ACTION_DOWN时,设置isClick变量为true;
- 当MotionEvent.ACTION_MOVE时,根据移动的距离distance与设备能识别的最小移动距离filter作对比,如果distance>filter,则isClick变量为false,否则为true;
- 当MotionEvent.ACTION_UP时,根据isClick的值判断是点击还是移动事件,若为点击事件则将传递给监听者,若为移动事件就进行轮播操作;
底部轮播圆点的布局及切换过程
实现底部圆点以及圆点的切换功能的实现思想:
- 需要自定义一个继承FramLayout的布局,利用FrameLayout的特性(在同一个位置放置不同的View,显示最后放入的View),我们就可以实现底部圆点的布局;
- 我们需要准备素材,底部圆点的素材,利用Drawable实现两种效果的图片;
- 需要自定义一个类继承FrameLayout,在该类的实现过程中,加载自定义的LooPViewGroup类和我们需要实现的底部圆点的布局LinearLayout来实现;
关于自定义类ImageLoopFrameLayout,有以下几个要点需要注意:
- 先加载LoopViewGroup布局,后加载圆点布局LinearLayout;
- 既然加载了LoopViewGroup,就需要对其点击事件进行监听,并且需要对图片轮播事件进行监听,做出相应的处理;
- 对调用者提供接口
addBitmaps(List<Bitmap> bitmaps)
方法,以便调用者可以设置轮播的图片,LooPViewGroup布局每增加一个图片,LinearLayout布局就必须对应增加一个圆点; - 对调用者提供点击事件的接口,以便调用者可以监听点击事件。
以上,轮播效果就全部完成了,详细的代码请参阅:
https://github.com/jemon9/CustomizeVIew/blob/master/viewlib/src/main/java/imageloader/zzf/com/viewlib/ImageLoopFrameLayout.java
https://github.com/jemon9/CustomizeVIew/blob/master/viewlib/src/main/java/imageloader/zzf/com/viewlib/LoopViewGroup.java