使用過Android內建的gallery組件的人都知道,gallery實現的效果就是拖動瀏覽一組圖片,相比iphone裡也是用於拖動瀏覽圖片的coverflow,顯然遜色不少。實際上,可以通過擴充gallery,通過偽3D變換可以基本實現coverflow的效果。本文通過原始碼解析這一功能的實現。具體代碼作用可參照注釋。
要使用gallery,我們必須首先給其指定一個adapter。在這裡,我們實現了一個自訂的ImageAdapter,為圖片製作倒影效果。
傳入參數為context和程式內drawable中的圖片ID數組。之後調用其中的createReflectedImages()方法分別創造每一個映像的倒影效果,產生對應的ImageView數組,最後在getView()中返回。
/** Copyright (C) 2010 Neil Davies** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.* * This code is base on the Android Gallery widget and was Created * by Neil Davies neild001 'at' gmail dot com to be a Coverflow widget* * @author Neil Davies*/public class ImageAdapter extends BaseAdapter {int mGalleryItemBackground;private Context mContext;private Integer[] mImageIds ;private ImageView[] mImages;public ImageAdapter(Context c, int[] ImageIds) {mContext = c;mImageIds = ImageIds;mImages = new ImageView[mImageIds.length];}public boolean createReflectedImages() {// The gap we want between the reflection and the original imagefinal int reflectionGap = 4;int index = 0;for (int imageId : mImageIds) {Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), imageId);int width = originalImage.getWidth();int height = originalImage.getHeight();// This will not scale but will flip on the Y axisMatrix matrix = new Matrix();matrix.preScale(1, -1);// Create a Bitmap with the flip matrix applied to it.// We only want the bottom half of the imageBitmap reflectionImage = Bitmap.createBitmap(originalImage, 0,height / 2, width, height / 2, matrix, false);// Create a new bitmap with same width but taller to fit// reflectionBitmap bitmapWithReflection = Bitmap.createBitmap(width,(height + height / 2), Config.ARGB_8888);// Create a new Canvas with the bitmap that's big enough for// the image plus gap plus reflectionCanvas canvas = new Canvas(bitmapWithReflection);// Draw in the original imagecanvas.drawBitmap(originalImage, 0, 0, null);// Draw in the gapPaint deafaultPaint = new Paint();canvas.drawRect(0, height, width, height + reflectionGap,deafaultPaint);// Draw in the reflectioncanvas.drawBitmap(reflectionImage, 0, height + reflectionGap,null);// Create a shader that is a linear gradient that covers the// reflectionPaint paint = new Paint();LinearGradient shader = new LinearGradient(0,originalImage.getHeight(), 0,bitmapWithReflection.getHeight() + reflectionGap,0x70ffffff, 0x00ffffff, TileMode.CLAMP);// Set the paint to use this shader (linear gradient)paint.setShader(shader);// Set the Transfer mode to be porter duff and destination inpaint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));// Draw a rectangle using the paint with our linear gradientcanvas.drawRect(0, height, width,bitmapWithReflection.getHeight() + reflectionGap, paint);ImageView imageView = new ImageView(mContext);imageView.setImageBitmap(bitmapWithReflection);imageView.setLayoutParams(new GalleryFlow.LayoutParams(160, 240));// imageView.setScaleType(ScaleType.MATRIX);mImages[index++] = imageView;}return true;}public int getCount() {return mImageIds.length;}public Object getItem(int position) {return position;}public long getItemId(int position) {return position;}public View getView(int position, View convertView, ViewGroup parent) {// Use this code if you want to load from resources/** ImageView i = new ImageView(mContext);* i.setImageResource(mImageIds[position]); i.setLayoutParams(new* CoverFlow.LayoutParams(350,350));* i.setScaleType(ImageView.ScaleType.CENTER_INSIDE);* * //Make sure we set anti-aliasing otherwise we get jaggies* BitmapDrawable drawable = (BitmapDrawable) i.getDrawable();* drawable.setAntiAlias(true); return i;*/return mImages[position];}/*** Returns the size (0.0f to 1.0f) of the views depending on the* 'offset' to the center.*/public float getScale(boolean focused, int offset) {/* Formula: 1 / (2 ^ offset) */return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset)));}}}
僅僅實現了圖片的倒影效果還不夠,因為在coverflow中圖片切換是有旋轉和縮放效果的,而內建的gallery中並沒有實現。因此,我們擴充內建的gallery,實現自己的galleryflow。在原gallery類中,提供了一個方法getChildStaticTransformation()以實現對圖片的變換。我們通過覆寫這個方法並在其中調用自訂的transformImageBitmap(“每個圖片與gallery中心的距離”)方法,,即可實現每個圖片做相應的旋轉和縮放。其中使用了camera和matrix用於視圖變換。具體可參考代碼注釋
public class GalleryFlow extends Gallery { /** * Graphics Camera used for transforming the matrix of ImageViews */ private Camera mCamera = new Camera(); /** * The maximum angle the Child ImageView will be rotated by */ private int mMaxRotationAngle = 60; /** * The maximum zoom on the centre Child */ private int mMaxZoom = -120; /** * The Centre of the Coverflow */ private int mCoveflowCenter; public GalleryFlow(Context context) { super(context); this.setStaticTransformationsEnabled(true); } public GalleryFlow(Context context, AttributeSet attrs) { super(context, attrs); this.setStaticTransformationsEnabled(true); } public GalleryFlow(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.setStaticTransformationsEnabled(true); } /** * Get the max rotational angle of the image * * @return the mMaxRotationAngle */ public int getMaxRotationAngle() { return mMaxRotationAngle; } /** * Set the max rotational angle of each image * * @param maxRotationAngle * the mMaxRotationAngle to set */ public void setMaxRotationAngle(int maxRotationAngle) { mMaxRotationAngle = maxRotationAngle; } /** * Get the Max zoom of the centre image * * @return the mMaxZoom */ public int getMaxZoom() { return mMaxZoom; } /** * Set the max zoom of the centre image * * @param maxZoom * the mMaxZoom to set */ public void setMaxZoom(int maxZoom) { mMaxZoom = maxZoom; } /** * Get the Centre of the Coverflow * * @return The centre of this Coverflow. */ private int getCenterOfCoverflow() { return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft(); } /** * Get the Centre of the View * * @return The centre of the given view. */ private static int getCenterOfView(View view) { return view.getLeft() + view.getWidth() / 2; } /** * {@inheritDoc} * * @see #setStaticTransformationsEnabled(boolean) */ protected boolean getChildStaticTransformation(View child, Transformation t) { final int childCenter = getCenterOfView(child); final int childWidth = child.getWidth(); int rotationAngle = 0; t.clear(); t.setTransformationType(Transformation.TYPE_MATRIX); if (childCenter == mCoveflowCenter) { transformImageBitmap((ImageView) child, t, 0); } else { rotationAngle = (int) (((float) (mCoveflowCenter - childCenter) / childWidth) * mMaxRotationAngle); if (Math.abs(rotationAngle) > mMaxRotationAngle) { rotationAngle = (rotationAngle < 0) ? -mMaxRotationAngle : mMaxRotationAngle; } transformImageBitmap((ImageView) child, t, rotationAngle); } return true; } /** * This is called during layout when the size of this view has changed. If * you were just added to the view hierarchy, you're called with the old * values of 0. * * @param w * Current width of this view. * @param h * Current height of this view. * @param oldw * Old width of this view. * @param oldh * Old height of this view. */ protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCoveflowCenter = getCenterOfCoverflow(); super.onSizeChanged(w, h, oldw, oldh); } /** * Transform the Image Bitmap by the Angle passed * * @param imageView * ImageView the ImageView whose bitmap we want to rotate * @param t * transformation * @param rotationAngle * the Angle by which to rotate the Bitmap */ private void transformImageBitmap(ImageView child, Transformation t, int rotationAngle) { mCamera.save(); final Matrix imageMatrix = t.getMatrix(); final int imageHeight = child.getLayoutParams().height; final int imageWidth = child.getLayoutParams().width; final int rotation = Math.abs(rotationAngle); // 在Z軸上正向移動camera的視角,實際效果為放大圖片。 // 如果在Y軸上移動,則圖片上下移動;X軸上對應圖片左右移動。 mCamera.translate(0.0f, 0.0f, 100.0f); // As the angle of the view gets less, zoom in if (rotation < mMaxRotationAngle) { float zoomAmount = (float) (mMaxZoom + (rotation * 1.5)); mCamera.translate(0.0f, 0.0f, zoomAmount); } // 在Y軸上旋轉,對應圖片豎向向裡翻轉。 // 如果在X軸上旋轉,則對應圖片橫向向裡翻轉。 mCamera.rotateY(rotationAngle); mCamera.getMatrix(imageMatrix); imageMatrix.preTranslate(-(imageWidth / 2), -(imageHeight / 2)); imageMatrix.postTranslate((imageWidth / 2), (imageHeight / 2)); mCamera.restore(); }}
代碼到這裡就結束了。有興趣的話可以自行調整裡面的參數來實現更多更炫的效果。
下面是調用的樣本:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_gallery); Integer[] images = { R.drawable.img0001, R.drawable.img0030, R.drawable.img0100, R.drawable.img0130, R.drawable.img0200, R.drawable.img0230, R.drawable.img0300, R.drawable.img0330, R.drawable.img0354 }; ImageAdapter adapter = new ImageAdapter(this, images); adapter.createReflectedImages(); GalleryFlow galleryFlow = (GalleryFlow) findViewById(R.id.gallery_flow); galleryFlow.setAdapter(adapter); }
PS1:
可以看出來這樣實現的gallery鋸齒問題比較嚴重。可以在createReflectedImages()使用以下代碼:
BitmapDrawable bd = new BitmapDrawable(bitmapWithReflection);bd.setAntiAlias(true);...
然後用iv.setImageDrawable(bd);
代替iv.setImageBitmap(bitmapWithReflection);
即可基本消除鋸齒。
PS2:
ImageAdapter有待確定的MemoryLeak問題,貌似的Bitmap的decode方法會造成ML,使用ImageAdapter時多次旋轉螢幕後會出現OOM。目前可以通過將使用完畢的bimap調用recycle()方法和設定null並及時調用system.gc()得到一些改善,但是問題並不明顯。
慶祝精華和推薦,增加3個PS~
PS3 ON PS1:
256樓說到,為什麼開啟消除鋸齒後不明顯。答案是,鋸齒不可能完全消除,但開啟消除鋸齒後會有很大改善。
另外還說到為什麼android不預設開啟鋸齒,以下是我的一點想法:
插值是我現在所知道的消除鋸齒的演算法,也就是計算像素間的相關度對其間插入中間像素以達到平滑映像邊緣的效果。但這無疑會耗費了大量的運算。
雖然我沒有經過測試,但是我猜測,使用antialias後圖形效能至少會下降30%。
當然,在這裡沒有涉及到複雜的圖形運算,所以開啟消除鋸齒不會有很明顯的效能影響,但如果你在模擬器或者低端機型上測試就會發現一點問題。
PS4:
有人問到transformImageBitmap()中這倆句話是什麼意思:
imageMatrix.preTranslate(-(imageWidth / 2), -(imageHeight / 2));
imageMatrix.postTranslate((imageWidth / 2), (imageHeight / 2));
個人的理解如下:
preTranslate相當於在對映像進行任何矩陣變換前先進行preTranslate,postTranslate相反,進行所有變換後再執行postTranlate。
這倆句的意思是:在做任何變換前,先將整個映像從映像的中心點移動到原點((0,0)點),執行變換完畢後再將映像從原點移動到之前的中心點。
如果不加這倆句,任何變換將以映像的原點為變換中心點,加了之後,任何變換都將以映像的中心點為變換中心點。
舉個例子,對映像進行旋轉,需要倆個參數:一個是旋轉的角度,另一個是旋轉中心的座標。旋轉中心的座標影響旋轉的效果。這個能明白嗎?你拿一根棍子,拿著棍子的一端進行旋轉和拿在棍子中間旋轉,是不一樣的。preTranslate和postTranslate執行後對映像本身不會有影響,影響的是對映像進行變換時的旋轉軸。
說了這麼多有點繞,其實就是矩陣變換的知識。
PS5 ON PS2:
這個問題在google group下有過很充分的討論,貌似一般只在debug模式下存在。現在我使用這段代碼沒有出現OOM問題了。
demo下載: Gallery.rar