在安卓开发中,图片轮播算是比较常用的控件。网上有很多人的各种各样的实现,比如有基于ViewPager的,这种实现总让人觉得太大材小用了,而且算不上是轮播,应该滚动到最后一页是没法再后翻的,这个时候会考虑两种补救措施:一种是直接smooth到第一次;另一种是后面新增和一个第一页一模一样的页面,这样就可以偷偷地在切换到最后一页时再执行smooth到第一次的操作,但这样可能需要自定义indicator指示器类,。。。。头晕。。。。 也有其他人是基于ViewFlipper,但别人的总是没有自己的好,于是下面是我的实现。
效果图:
超过200k无法上传!
实现的文件介绍:
src
|- me.lanfog.myandroid.widget
|- ImageFlipper 轮播控件类:支持触摸触发前后翻,默认自动从右向左轮翻
|- CycleIndicator 一个指示器:一排圆点,选中为实心,未选中为空心
|- Activity.java 测试示例:视图
|- res
|- anim
|- slide_in_left.xml 左边进入动画
|- slide_out_right.xml 右边出去动画
|- slide_in_right.xml 右边进入动画
|- slide_out_left.xml 左边出去动画
|- layout
|- layout.xml 测试实例 :布局文件
各个文件内容如下:
ImageFlipper.java
package me.lanfog.myandroid.widget; import me.lanfog.myandroid.R; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.ViewFlipper; public class ImageFlipper extends ViewFlipper { private Animation previousInAnimation, previousOutAnimation; // 向前切换时,进出动画 private Animation nextInAnimation, nextOutAnimation; // 前后切换时,进出动画 private boolean animRunning; private AnimationListener mAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { // TODO Auto-generated method stub animRunning = true; } @Override public void onAnimationRepeat(Animation animation) { // TODO Auto-generated method stub } @Override public void onAnimationEnd(Animation animation) { // TODO Auto-generated method stub animRunning = false; if(mOnPageChangeListener != null) mOnPageChangeListener.onPageSelected(indexOfChild(getCurrentView())); } }; private float firstX, deltaX; private int flipSpacing = 100; // 触摸触发切换,生效距离 private OnPageChangeListener mOnPageChangeListener; // 切换事件监听器 public ImageFlipper(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub init(); } public ImageFlipper(Context context) { super(context); // TODO Auto-generated constructor stub init(); } private void init(){ // 切换动画效果 setAnimation(R.anim.slide_in_left, R.anim.slide_out_right, R.anim.slide_in_right, R.anim.slide_out_left); // 切换间隔 setFlipInterval(2000); // 自动开始 setAutoStart(true); } @Override public void showPrevious() { setInAnimation(previousInAnimation); setOutAnimation(previousOutAnimation); super.showPrevious(); } @Override public void showNext() { setInAnimation(nextInAnimation); setOutAnimation(nextOutAnimation); super.showNext(); } public void setViews(View[] views){ for(View v:views) addView(v, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } public void setViews(int[] ids){ removeAllViews(); for(int id:ids){ addView(id); } } public void addView(int id) { ImageView iv = new ImageView(getContext()); iv.setImageResource(id); iv.setScaleType(ImageView.ScaleType.FIT_XY); addView(iv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent ev) { // 动画过程中,不响应触摸操作 if(animRunning) return true; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // 暂停自动切换 if(isFlipping()) stopFlipping(); firstX = ev.getX(); break; case MotionEvent.ACTION_UP: deltaX = ev.getX() - firstX; if(Math.abs(deltaX) > flipSpacing){ if(deltaX > 0) this.showPrevious(); else { this.showNext(); // 启动自动切换 // 起落间隔距离不足时,之后不会再有自动切换 this.startFlipping(); } } break; } return true; } public void setAnimation(Animation pin, Animation pout, Animation nin, Animation nout){ this.previousInAnimation = pin; this.previousOutAnimation = pout; this.nextInAnimation = nin; this.nextOutAnimation = nout; this.previousInAnimation.setAnimationListener(mAnimationListener); this.nextInAnimation.setAnimationListener(mAnimationListener); } public void setAnimation(int pin, int pout, int nin, int nout){ this.setAnimation( AnimationUtils.loadAnimation(getContext(), pin), AnimationUtils.loadAnimation(getContext(), pout), AnimationUtils.loadAnimation(getContext(), nin), AnimationUtils.loadAnimation(getContext(), nout)); } public OnPageChangeListener getOnPageChangeListener() { return mOnPageChangeListener; } public void setOnPageChangeListener(OnPageChangeListener mOnPageChangeListener) { this.mOnPageChangeListener = mOnPageChangeListener; } /** * 页面切换监听器 */ public static interface OnPageChangeListener { public void onPageSelected(int index); } }
其中,默认会使用到四个动画效果:
slide_in_left.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromXDelta="-100%p" android:toXDelta="0"/>
slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromXDelta="0" android:toXDelta="100%p"/>
slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromXDelta="100%p" android:toXDelta="0" />
slide_out_left.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromXDelta="0" android:toXDelta="-100%p" />
为了好看一点,再实现一个指示器类:
CycleIndicator
package me.lanfog.myandroid.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; public class CycleIndicator extends View { private int pageCount; // 总页数 private int pageSelected; // 当前页 private int radius = 5; // 半径 private int stokenWidth = 2; // 边框宽度 private int gap = 3; // 间隔 private Paint mPaint; public CycleIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub init(); } public CycleIndicator(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub init(); } public CycleIndicator(Context context) { super(context); // TODO Auto-generated constructor stub init(); } private void init() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStrokeWidth(stokenWidth); mPaint.setColor(Color.WHITE); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(), measureHeight()); } protected int measureWidth() { return ( radius + stokenWidth ) * 2 * pageCount + gap * ( pageCount - 1 ); } protected int measureHeight() { return ( radius + stokenWidth ) * 2; } @Override protected void onDraw(Canvas canvas) { drawIndicator(canvas); } private void drawIndicator(Canvas canvas) { for(int i=0;i<pageCount;i++) { if(i == pageSelected) mPaint.setStyle(Paint.Style.FILL_AND_STROKE); else mPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(( radius + stokenWidth ) * ( 2 * i + 1 ) + gap * i , radius + stokenWidth , radius, mPaint); } } public void setPageCount(int count){ this.pageCount = count; this.requestLayout(); } public void onPageSelected(int index){ this.pageSelected = index; this.postInvalidate(); } public int getRadius() { return radius; } public void setRadius(int radius) { this.radius = radius; } public int getStokenWidth() { return stokenWidth; } public void setStokenWidth(int stokenWidth) { this.stokenWidth = stokenWidth; } public int getGap() { return gap; } public void setGap(int gap) { this.gap = gap; } }
以下是测试类:
activity.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_green_dark" android:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" android:layout_height="100dp"> <me.lanfog.myandroid.widget.ImageFlipper android:id="@+id/flipper0" android:layout_width="match_parent" android:layout_height="100dp" /> <me.lanfog.myandroid.widget.CycleIndicator android:id="@+id/indicator0" android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" android:layout_marginBottom="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout> <FrameLayout android:layout_weight="1" android:layout_width="match_parent" android:background="@android:color/white" android:layout_height="0dp"/> </LinearLayout>
Activity.java
final CycleIndicator indicator0 = (CycleIndicator) findViewById(R.id.indicator0); final ImageFlipper flipper0 = (ImageFlipper) findViewById(R.id.flipper0); flipper0.setViews(new int[]{R.drawable.p0, R.drawable.p1, R.drawable.p2}); indicator0.setPageCount(3); flipper0.setOnPageChangeListener(new ImageFlipper.OnPageChangeListener() { @Override public void onPageSelected(int index) { indicator0.onPageSelected(index); Toast.makeText(FlipperDemo.this, "index=" + index, 20).show(); } });
其他动画切换效果:
上下翻动:
slide_in_top.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromYDelta="-100%p" android:toYDelta="0"/>
slide_out_bottom.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromYDelta="0" android:toYDelta="100%p"/>
slide_in_bottom.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromYDelta="100%p" android:toYDelta="0" />
slide_out_top.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromYDelta="0" android:toYDelta="-100%p" />
flipper0.setAnimation(R.anim.slide_in_bottom, R.anim.slide_out_top, R.anim.slide_in_top, R.anim.slide_out_bottom);
淡入淡出:
fade_in.xml
<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromAlpha="0" android:toAlpha="1" />
fade_out.xml
<?xml version="1.0" encoding="utf-8"?> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:fromAlpha="1" android:toAlpha="0" />
flipper0.setAnimation(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
总结:
测试时候比较基于ViewPager和ViewFlipper两种实现方式发现,除了之前是否支持轮放这一特征之外,
最大的一个区别是:前者存在中间状态,而后者没有,即在触摸滑动过程中,前者会存在两张图片会同时静态地存在,而后则会仅在动画过程中才有。对此,一个可能完善的逻辑是:修正触摸事件的处理,在move过程中,判断移动距离达到指定值,则触发切换,如下:
switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // 暂停自动切换 if(isFlipping()) stopFlipping(); firstX = ev.getX(); break; case MotionEvent.ACTION_MOVE: if(firstX == -1) break; deltaX = ev.getX() - firstX; if(Math.abs(deltaX) > flipSpacing){ if(deltaX > 0) this.showPrevious(); else { this.showNext(); // 成功触后翻之后,刽自动后翻 this.startFlipping(); } firstX = -1; } break; }