Android改进版CoverFlow效果控件

  最近研究了一下如何在Android上实现CoverFlow效果的控件,其实早在2010年,就有Neil Davies开发并开源出了这个控件,Neil大神的这篇博客地址http://www.inter-fuser.com/2010/02/android-coverflow-widget-v2.html。首先是阅读源码,弄明白核心思路后,自己重新写了一遍这个控件,并加入了详尽的注释以便日后查阅;而后在使用过程中,发现了有两点可以改进:(1)初始图片位于中间,左边空了一半空间,比较难看,可以改为重复滚动地展示、(2)由于图片一开始就需要加载出来,所以对内存开销较大,很容易OOM,需要对图片的内存空间进行压缩。

  这个自定义控件包括4个部分,用于创建及提供图片对象的ImageAdapter,计算图片旋转角度等的自定义控件GalleryFlow,压缩采样率解析Bitmap的工具类BitmapScaleDownUtil,以及承载自定义控件的Gallery3DActivity。

  首先是ImageAdapter,代码如下:

  1 package pym.test.gallery3d.widget;
  2 
  3 import pym.test.gallery3d.util.BitmapScaleDownUtil;
  4 import android.content.Context;
  5 import android.graphics.Bitmap;
  6 import android.graphics.Bitmap.Config;
  7 import android.graphics.Canvas;
  8 import android.graphics.LinearGradient;
  9 import android.graphics.Matrix;
 10 import android.graphics.Paint;
 11 import android.graphics.PaintFlagsDrawFilter;
 12 import android.graphics.PorterDuff.Mode;
 13 import android.graphics.PorterDuffXfermode;
 14 import android.graphics.Shader.TileMode;
 15 import android.view.View;
 16 import android.view.ViewGroup;
 17 import android.widget.BaseAdapter;
 18 import android.widget.Gallery;
 19 import android.widget.ImageView;
 20 
 21 /**
 22  * @author pengyiming
 23  * @date 2013-9-30
 24  * @function GalleryFlow适配器
 25  */
 26 public class ImageAdapter extends BaseAdapter
 27 {
 28     /* 数据段begin */
 29     private final String TAG = "ImageAdapter";
 30     private Context mContext;
 31     
 32     //图片数组
 33     private int[] mImageIds ;
 34     //图片控件数组
 35     private ImageView[] mImages;
 36     //图片控件LayoutParams
 37     private GalleryFlow.LayoutParams mImagesLayoutParams;
 38     /* 数据段end */
 39 
 40     /* 函数段begin */
 41     public ImageAdapter(Context context, int[] imageIds)
 42     {
 43         mContext = context;
 44         mImageIds = imageIds;
 45         mImages = new ImageView[mImageIds.length];
 46         mImagesLayoutParams = new GalleryFlow.LayoutParams(Gallery.LayoutParams.WRAP_CONTENT, Gallery.LayoutParams.WRAP_CONTENT);
 47     }
 48     
 49     /**
 50      * @function 根据指定宽高创建待绘制的Bitmap,并绘制到ImageView控件上
 51      * @param imageWidth
 52      * @param imageHeight
 53      * @return void
 54      */
 55     public void createImages(int imageWidth, int imageHeight)
 56     {
 57         // 原图与倒影的间距5px
 58         final int gapHeight = 5;
 59         
 60         int index = 0;
 61         for (int imageId : mImageIds)
 62         {
 63             /* step1 采样方式解析原图并生成倒影 */
 64             // 解析原图,生成原图Bitmap对象
 65 //            Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), imageId);
 66             Bitmap originalImage = BitmapScaleDownUtil.decodeSampledBitmapFromResource(mContext.getResources(), imageId, imageWidth, imageHeight);
 67             int width = originalImage.getWidth();
 68             int height = originalImage.getHeight();
 69             
 70             // Y轴方向反向,实质就是X轴翻转
 71             Matrix matrix = new Matrix();
 72             matrix.setScale(1, -1);
 73             // 且仅取原图下半部分创建倒影Bitmap对象
 74             Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, height / 2, width, height / 2, matrix, false);
 75             
 76             /* step2 绘制 */
 77             // 创建一个可包含原图+间距+倒影的新图Bitmap对象
 78             Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + gapHeight + height / 2), Config.ARGB_8888);
 79             // 在新图Bitmap对象之上创建画布
 80             Canvas canvas = new Canvas(bitmapWithReflection);
 81             // 抗锯齿效果
 82             canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG));
 83             // 绘制原图
 84             canvas.drawBitmap(originalImage, 0, 0, null);
 85             // 绘制间距
 86             Paint gapPaint = new Paint();
 87             gapPaint.setColor(0xFFCCCCCC);
 88             canvas.drawRect(0, height, width, height + gapHeight, gapPaint);
 89             // 绘制倒影
 90             canvas.drawBitmap(reflectionImage, 0, height + gapHeight, null);
 91             
 92             /* step3 渲染 */
 93             // 创建一个线性渐变的渲染器用于渲染倒影
 94             Paint paint = new Paint();
 95             LinearGradient shader = new LinearGradient(0, height, 0, (height + gapHeight + height / 2), 0x70ffffff, 0x00ffffff, TileMode.CLAMP);
 96             // 设置画笔渲染器
 97             paint.setShader(shader);
 98             // 设置图片混合模式
 99             paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));
100             // 渲染倒影+间距
101             canvas.drawRect(0, height, width, (height + gapHeight + height / 2), paint);
102             
103             /* step4 在ImageView控件上绘制 */
104             ImageView imageView = new ImageView(mContext);
105             imageView.setImageBitmap(bitmapWithReflection);
106             imageView.setLayoutParams(mImagesLayoutParams);
107             // 打log
108             imageView.setTag(index);
109             
110             /* step5 释放heap */
111             originalImage.recycle();
112             reflectionImage.recycle();
113 //          bitmapWithReflection.recycle();
114             
115             mImages[index++] = imageView;
116         }
117     }
118 
119     @Override
120     public int getCount()
121     {
122         return Integer.MAX_VALUE;
123     }
124     
125     @Override
126     public Object getItem(int position)
127     {
128         return mImages[position];
129     }
130     
131     @Override
132     public long getItemId(int position)
133     {
134         return position;
135     }
136     
137     @Override
138     public View getView(int position, View convertView, ViewGroup parent)
139     {
140         return mImages[position % mImages.length];
141     }
142     /* 函数段end */
143 }

  其次是GalleryFlow,代码如下:

  1 package pym.test.gallery3d.widget;
  2 
  3 import android.content.Context;
  4 import android.graphics.Camera;
  5 import android.graphics.Matrix;
  6 import android.util.AttributeSet;
  7 import android.util.Log;
  8 import android.view.View;
  9 import android.view.animation.Transformation;
 10 import android.widget.Gallery;
 11 
 12 /**
 13  * @author pengyiming
 14  * @date 2013-9-30
 15  * @function 自定义控件
 16  */
 17 public class GalleryFlow extends Gallery
 18 {
 19     /* 数据段begin */
 20     private final String TAG = "GalleryFlow";
 21     
 22     // 边缘图片最大旋转角度
 23     private final float MAX_ROTATION_ANGLE = 75;
 24     // 中心图片最大前置距离
 25     private final float MAX_TRANSLATE_DISTANCE = -100;
 26     // GalleryFlow中心X坐标
 27     private int mGalleryFlowCenterX;
 28     // 3D变换Camera
 29     private Camera mCamera = new Camera();
 30     /* 数据段end */
 31 
 32     /* 函数段begin */
 33     public GalleryFlow(Context context, AttributeSet attrs)
 34     {
 35         super(context, attrs);
 36         
 37         // 开启,在滑动过程中,回调getChildStaticTransformation()
 38         this.setStaticTransformationsEnabled(true);
 39     }
 40     
 41     /**
 42      * @function 获取GalleryFlow中心X坐标
 43      * @return
 44      */
 45     private int getCenterXOfCoverflow()
 46     {
 47         return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft();
 48     }
 49     
 50     /**
 51      * @function 获取GalleryFlow子view的中心X坐标
 52      * @param childView
 53      * @return
 54      */
 55     private int getCenterXOfView(View childView)
 56     {
 57         return childView.getLeft() + childView.getWidth() / 2;
 58     }
 59     
 60     /**
 61      * @note step1 系统调用measure()方法时,回调此方法;表明此时系统正在计算view的大小
 62      */
 63     @Override
 64     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
 65     {
 66         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 67         
 68         mGalleryFlowCenterX = getCenterXOfCoverflow();
 69         Log.d(TAG, "onMeasure, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
 70     }
 71     
 72     /**
 73      * @note step2 系统调用layout()方法时,回调此方法;表明此时系统正在给child view分配空间
 74      * @note 必定在onMeasure()之后回调,但与onSizeChanged()先后顺序不一定
 75      */
 76     @Override
 77     protected void onLayout(boolean changed, int l, int t, int r, int b)
 78     {
 79         super.onLayout(changed, l, t, r, b);
 80         
 81         mGalleryFlowCenterX = getCenterXOfCoverflow();
 82         Log.d(TAG, "onLayout, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
 83     }
 84     
 85     /**
 86      * @note step2 系统调用measure()方法后,当需要绘制此view时,回调此方法;表明此时系统已计算完view的大小
 87      * @note 必定在onMeasure()之后回调,但与onSizeChanged()先后顺序不一定
 88      */
 89     @Override
 90     protected void onSizeChanged(int w, int h, int oldw, int oldh)
 91     {
 92         super.onSizeChanged(w, h, oldw, oldh);
 93         
 94         mGalleryFlowCenterX = getCenterXOfCoverflow();
 95         Log.d(TAG, "onSizeChanged, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
 96     }
 97     
 98     @Override
 99     protected boolean getChildStaticTransformation(View childView, Transformation t)
100     {
101         // 计算旋转角度
102         float rotationAngle = calculateRotationAngle(childView);
103         
104         // 计算前置距离
105         float translateDistance = calculateTranslateDistance(childView);
106         
107         // 开始3D变换
108         transformChildView(childView, t, rotationAngle, translateDistance);
109         
110         return true;
111     }
112     
113     /**
114      * @function 计算GalleryFlow子view的旋转角度
115      * @note1 位于Gallery中心的图片不旋转
116      * @note2 位于Gallery中心两侧的图片按照离中心点的距离旋转
117      * @param childView
118      * @return
119      */
120     private float calculateRotationAngle(View childView)
121     {
122         final int childCenterX = getCenterXOfView(childView);
123         float rotationAngle = 0;
124         
125         rotationAngle = (mGalleryFlowCenterX - childCenterX) / (float) mGalleryFlowCenterX * MAX_ROTATION_ANGLE;
126         
127         if (rotationAngle > MAX_ROTATION_ANGLE)
128         {
129             rotationAngle = MAX_ROTATION_ANGLE;
130         }
131         else if (rotationAngle < -MAX_ROTATION_ANGLE)
132         {
133             rotationAngle = -MAX_ROTATION_ANGLE;
134         }
135         
136         return rotationAngle;
137     }
138     
139     /**
140      * @function 计算GalleryFlow子view的前置距离
141      * @note1 位于Gallery中心的图片前置
142      * @note2 位于Gallery中心两侧的图片不前置
143      * @param childView
144      * @return
145      */
146     private float calculateTranslateDistance(View childView)
147     {
148         final int childCenterX = getCenterXOfView(childView);
149         float translateDistance = 0;
150         
151         if (mGalleryFlowCenterX == childCenterX)
152         {
153             translateDistance = MAX_TRANSLATE_DISTANCE;
154         }
155         
156         return translateDistance;
157     }
158     
159     /**
160      * @function 开始变换GalleryFlow子view
161      * @param childView
162      * @param t
163      * @param rotationAngle
164      * @param translateDistance
165      */
166     private void transformChildView(View childView, Transformation t, float rotationAngle, float translateDistance)
167     {
168         t.clear();
169         t.setTransformationType(Transformation.TYPE_MATRIX);
170         
171         final Matrix imageMatrix = t.getMatrix();
172         final int imageWidth = childView.getWidth();
173         final int imageHeight = childView.getHeight();
174         
175         mCamera.save();
176         
177         /* rotateY */
178         // 在Y轴上旋转,位于中心的图片不旋转,中心两侧的图片竖向向里或向外翻转。
179         mCamera.rotateY(rotationAngle);
180         /* rotateY */
181         
182         /* translateZ */
183         // 在Z轴上前置,位于中心的图片会有放大的效果
184         mCamera.translate(0, 0, translateDistance);
185         /* translateZ */
186         
187         // 开始变换(我的理解是:移动Camera,在2D视图上产生3D效果)
188         mCamera.getMatrix(imageMatrix);
189         imageMatrix.preTranslate(-imageWidth / 2, -imageHeight / 2);
190         imageMatrix.postTranslate(imageWidth / 2, imageHeight / 2);
191         
192         mCamera.restore();
193     }
194     /* 函数段end */
195 }

  Bitmap解析用具BitmapScaleDownUtil,代码如下:

 1 package pym.test.gallery3d.util;
 2 
 3 import android.content.res.Resources;
 4 import android.graphics.Bitmap;
 5 import android.graphics.BitmapFactory;
 6 import android.view.Display;
 7 
 8 /**
 9  * @author pengyiming
10  * @date 2013-9-30
11  * @function Bitmap缩放处理工具类
12  */
13 public class BitmapScaleDownUtil
14 {
15     /* 数据段begin */
16     private final String TAG = "BitmapScaleDownUtil";
17     /* 数据段end */
18 
19     /* 函数段begin */
20     /**
21      * @function 获取屏幕大小
22      * @param display
23      * @return 屏幕宽高
24      */
25     public static int[] getScreenDimension(Display display)
26     {
27         int[] dimension = new int[2];
28         dimension[0] = display.getWidth();
29         dimension[1] = display.getHeight();
30         
31         return dimension;
32     }
33     
34     /**
35      * @function 以取样方式加载Bitmap 
36      * @param res
37      * @param resId
38      * @param reqWidth
39      * @param reqHeight
40      * @return 取样后的Bitmap
41      */
42     public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
43     {
44         // step1,将inJustDecodeBounds置为true,以解析Bitmap真实尺寸
45         final BitmapFactory.Options options = new BitmapFactory.Options();
46         options.inJustDecodeBounds = true;
47         BitmapFactory.decodeResource(res, resId, options);
48 
49         // step2,计算Bitmap取样比例
50         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
51 
52         // step3,将inJustDecodeBounds置为false,以取样比列解析Bitmap
53         options.inJustDecodeBounds = false;
54         return BitmapFactory.decodeResource(res, resId, options);
55     }
56 
57     /**
58      * @function 计算Bitmap取样比例
59      * @param options
60      * @param reqWidth
61      * @param reqHeight
62      * @return 取样比例
63      */
64     private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
65     {
66         // 默认取样比例为1:1
67         int inSampleSize = 1;
68 
69         // Bitmap原始尺寸
70         final int width = options.outWidth;
71         final int height = options.outHeight;
72 
73         // 取最大取样比例
74         if (height > reqHeight || width > reqWidth)
75         {
76             final int widthRatio = Math.round((float) width / (float) reqWidth);
77             final int heightRatio = Math.round((float) height / (float) reqHeight);
78 
79             // 取样比例为X:1,其中X>=1
80             inSampleSize = Math.max(widthRatio, heightRatio);
81         }
82 
83         return inSampleSize;
84     }
85     /* 函数段end */
86 }

  测试控件的Gallery3DActivity,代码如下:

 1 package pym.test.gallery3d.main;
 2 
 3 import pym.test.gallery3d.R;
 4 import pym.test.gallery3d.util.BitmapScaleDownUtil;
 5 import pym.test.gallery3d.widget.GalleryFlow;
 6 import pym.test.gallery3d.widget.ImageAdapter;
 7 import android.app.Activity;
 8 import android.content.Context;
 9 import android.os.Bundle;
10 
11 /**
12  * @author pengyiming
13  * @date 2013-9-30
14  */
15 public class Gallery3DActivity extends Activity
16 {
17     /* 数据段begin */
18     private final String TAG = "Gallery3DActivity";
19     private Context mContext;
20     
21     // 图片缩放倍率(相对屏幕尺寸的缩小倍率)
22     public static final int SCALE_FACTOR = 8;
23     
24     // 图片间距(控制各图片之间的距离)
25     private final int GALLERY_SPACING = -10;
26     
27     // 控件
28     private GalleryFlow mGalleryFlow;
29     /* 数据段end */
30 
31     /* 函数段begin */
32     @Override
33     protected void onCreate(Bundle savedInstanceState)
34     {
35         super.onCreate(savedInstanceState);
36         mContext = getApplicationContext();
37         
38         setContentView(R.layout.gallery_3d_activity_layout);
39         initGallery();
40     }
41     
42     private void initGallery()
43     {
44         // 图片ID
45         int[] images = {
46                 R.drawable.picture_1,
47                 R.drawable.picture_2,
48                 R.drawable.picture_3,
49                 R.drawable.picture_4,
50                 R.drawable.picture_5,
51                 R.drawable.picture_6,
52                 R.drawable.picture_7 };
53 
54         ImageAdapter adapter = new ImageAdapter(mContext, images);
55         // 计算图片的宽高
56         int[] dimension = BitmapScaleDownUtil.getScreenDimension(getWindowManager().getDefaultDisplay());
57         int imageWidth = dimension[0] / SCALE_FACTOR;
58         int imageHeight = dimension[1] / SCALE_FACTOR;
59         // 初始化图片
60         adapter.createImages(imageWidth, imageHeight);
61 
62         // 设置Adapter,显示位置位于控件中间,这样使得左右均可"无限"滑动
63         mGalleryFlow = (GalleryFlow) findViewById(R.id.gallery_flow);
64         mGalleryFlow.setSpacing(GALLERY_SPACING);
65         mGalleryFlow.setAdapter(adapter);
66         mGalleryFlow.setSelection(Integer.MAX_VALUE / 2);
67     }
68     /* 函数段end */
69 }

  see效果图~~~

 

posted @ 2013-10-21 17:24  热气球  阅读(20559)  评论(4编辑  收藏  举报