最近研究了一下如何在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效果圖~~~