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效果图~~~