Android 高级UI设计笔记14:Gallery(画廊控件)之 3D图片浏览
1. 利用Gallery组件实现 3D图片浏览器的功能,如下:
2. 下面是详细的实现过程如下:
(1)这里我是测试性代码,我的图片是自己添加到res/drawable/目录下的,如下:
但是开发中不能是这样,往往是浏览手机中的图片,比如浏览手机中SD卡中的图片,这里我们需要写一个方法 getImagesFromSD()获取SD卡中的图片信息(图片的SD路径),代码如下:
1 private List<String> getImagesFromSD() { 2 List<String> imageList = new ArrayList<String>(); 3 File f = Environment.getExternalStorageDirectory(); 4 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 5 f = new File(Environment.getExternalStorageDirectory().toString()); 6 } else { 7 Toast.makeText(MainActivity.this, R.string.sdcarderror, Toast.LENGTH_LONG).show(); 8 return imageList; 9 } 10 11 File[] files = f.listFiles(); 12 if (files == null || files.length == 0) 13 return imageList; 14 15 for (int i = 0; i < files.length; i++) { 16 File file = files[i]; 17 if (isImageFile(file.getPath())) 18 imageList.add(file.getPath()); 19 } 20 return imageList; 21 }
上面这个方法的作用,就是获取SD卡中,所有文件的后缀名满足图片后缀名的文件的路径,然后放到List容器中返回。
isImageFile方法是这样实现的:
1 private boolean isImageFile(String fName) { 2 String end = fName.substring(fName.lastIndexOf(".") + 1, fName.length()).toLowerCase(); 3 if (end.equals("jpg") || end.equals("gif") || end.equals("png") || end.equals("jpeg") || end.equals("bmp")) { 4 return true; 5 } 6 return false; 7 8 }
这里我们先实现Gallery 3D图片浏览的效果,暂时不用理会这个图片路径的问题。
(2)我们知道Android中自带的Gallery组件是不能实现3D浏览效果的,我们这里需要自定义一个GalleryView继承自Gallery,从而附加这个GalleryView
特别的功能:实现图片的 旋转 和 缩放 ( 两种功能集合起来,给用户感官上就是3D浏览器的感觉)
GalleryView如下:
1 package com.mythou.grallery3d; 2 3 import android.content.Context; 4 import android.graphics.Camera; 5 import android.graphics.Matrix; 6 import android.util.AttributeSet; 7 import android.view.View; 8 import android.view.animation.Transformation; 9 import android.widget.Gallery; 10 import android.widget.ImageView; 11 12 /** 13 * 自定义GalleryView: 14 * 主要做了对图片的旋转和缩放操作,根据图片的屏幕中的位置对其进行旋转缩放操作。 15 * @author hebao 16 * 17 */ 18 public class GalleryView extends Gallery { 19 // 相机类,用来做类3D效果处理,比如z轴方向上的平移,绕y轴的旋转等 20 private Camera mCamera = new Camera(); 21 // 最大转动角度,是图片绕y轴最大旋转角度,也就是屏幕最边上那两张图片的旋转角度 22 private int mMaxRotationAngle = 45; 23 // 最大缩放值,是图片在z轴平移的距离,视觉上看起来就是放大缩小的效果 24 private int mMaxZoom = -120; 25 // 半径值 26 private int mCoveflowCenter; 27 28 public GalleryView(Context context) { 29 super(context); 30 /** 设置为true时,允许多子类进行静态转换: 31 * 也就是说把这个属性设成true的时候每次viewGroup(看Gallery的源码就可以看到它是从ViewGroup间 32 * 接继承过来的)在重新画它的child的时候都会促发getChildStaticTransformation这个函数,所以我 33 * 们只需要在这个函数里面去加上旋转和放大的操作就可以了 34 **/ 35 this.setStaticTransformationsEnabled(true); 36 } 37 38 public GalleryView(Context context, AttributeSet attrs) { 39 super(context, attrs); 40 this.setStaticTransformationsEnabled(true); 41 } 42 43 public GalleryView(Context context, AttributeSet attrs, int defStyle) { 44 super(context, attrs, defStyle); 45 this.setStaticTransformationsEnabled(true); 46 } 47 48 public int getMaxRotationAngle() { 49 return mMaxRotationAngle; 50 } 51 52 public void setMaxRotationAngle(int maxRotationAngle) { 53 mMaxRotationAngle = maxRotationAngle; 54 } 55 56 public int getMaxZoom() { 57 return mMaxZoom; 58 } 59 60 public void setMaxZoom(int maxZoom) { 61 mMaxZoom = maxZoom; 62 } 63 64 65 //获取GalleryView的中心位置center 66 private int getCenterOfCoverflow() { 67 return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft(); 68 } 69 //获取子View的位置 70 private static int getCenterOfView(View view) { 71 return view.getLeft() + view.getWidth() / 2; 72 } 73 74 75 76 /** 77 * 在当前GalleryView的大小发生改变是被系统调用 78 */ 79 @Override 80 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 81 mCoveflowCenter = getCenterOfCoverflow(); 82 super.onSizeChanged(w, h, oldw, oldh); 83 } 84 85 /** 86 * 控制gallery中每个图片的旋转(重写的gallery中方法) 87 */ 88 @Override 89 protected boolean getChildStaticTransformation(View child, Transformation trans) { 90 //取得当前子view的半径值 91 final int childCenter = getCenterOfView(child); 92 System.out.println("childCenter:"+childCenter); 93 94 final int childWidth = child.getWidth(); 95 96 //旋转角度 97 int rotationAngle = 0; 98 //重置转换状态 99 trans.clear(); 100 /**设置转换类型 101 * TYPE_IDENTITY 102 * TYPE_ALPHA 103 * TYPE_MATRIX 104 * TYPE_BOTH 105 */ 106 trans.setTransformationType(Transformation.TYPE_BOTH); 107 108 //如果图片位于中心位置不需要进行旋转 109 if (childCenter == mCoveflowCenter) { 110 transformImageBitmap((ImageView) child, trans, 0); 111 } else { 112 //根据图片在gallery中的位置来计算图片的旋转角度 113 rotationAngle = (int) (((float) (mCoveflowCenter - childCenter) / childWidth) * mMaxRotationAngle); 114 System.out.println("rotationAngle:" +rotationAngle); 115 //如果旋转角度绝对值大于最大旋转角度返回 (-mMaxRotationAngle 或 mMaxRotationAngle ) 116 if (Math.abs(rotationAngle) > mMaxRotationAngle) { 117 rotationAngle = (rotationAngle < 0) ? -mMaxRotationAngle : mMaxRotationAngle; 118 } 119 transformImageBitmap((ImageView) child, trans, rotationAngle); 120 } 121 122 return true; 123 } 124 125 /** 126 * 127 * 128 * 旋转、缩放图片 129 * 130 * @param child 子View 131 * @param trans 变换,Transformation 中包含一个矩阵和 alpha 值,矩阵是用来做平移、旋转和缩放动画的, 132 * 通过Transformation来设置子控件的canvas。 133 * @param rotationAngle 旋转角度 134 * 135 * 备注:Transformation 136 * Transformation记录了仿射矩阵Matrix,动画每触发一次,会对原来的矩阵做一次运算, 137 * View的Bitmap与这个矩阵相乘就可以实现相应的操作(旋转、平移、缩放等)。 138 * Transformation类封装了矩阵和alpha值,它有两个重要的成员,一是mMatrix,二是mAlpha(控制透明度)。 139 * 140 */ 141 private void transformImageBitmap(ImageView child, Transformation trans, int rotationAngle) { 142 143 //对效果进行保存 144 mCamera.save(); 145 146 final Matrix imageMatrix = trans.getMatrix(); 147 //图片高度 148 final int imageHeight = child.getLayoutParams().height; 149 //图片宽度 150 final int imageWidth = child.getLayoutParams().width; 151 //返回旋转角度的绝对值 152 final int rotation = Math.abs(rotationAngle); 153 154 /** 155 * translate(float x, float y, float z) 156 * 三个参数x,y,z分别为在手机屏幕坐标系x,y,z三个轴上的位移,这三个参数为正:表示沿着相应坐标系正方向移动 157 * 158 * 在Z轴上负向移动camera的视角,实际效果为放大图片。 159 * 在Y轴上移动,则图片上下移动; X轴上对应图片左右移动。 160 */ 161 mCamera.translate(0.0f, 0.0f, -20.0f); 162 163 // As the angle of the view gets less, zoom in 164 if (rotation < mMaxRotationAngle) { 165 float zoomAmount = (float) (mMaxZoom + (rotation * 1.0)); 166 mCamera.translate(0.0f, 0.0f, zoomAmount); 167 } 168 169 // rotateY:在Y轴上旋转,沿着Y轴图片往里翻转,对应图片竖向向里翻转。 170 // rotateX:在X轴上旋转,沿着X轴图片往里翻转,则对应图片横向向里翻转。 171 mCamera.rotateY(rotationAngle); 172 173 mCamera.getMatrix(imageMatrix); 174 /** 175 * preTranslate(float dx, float dy) 176 * postTranslate(float dx, float dy) 177 * 注意他们参数是平移的距离,而不是平移目的地的坐标 178 */ 179 imageMatrix.preTranslate(-(imageWidth / 2), -(imageHeight / 2)); 180 imageMatrix.postTranslate((imageWidth / 2), (imageHeight / 2)); 181 //恢复相机状态 182 mCamera.restore(); 183 } 184 }
通过自定义gallery控件,现在我们已经实现了当滑动Gallery里面的图片时,两侧的图片会发生一定角度的旋转,也就是完成了3D效果的第一部,下一步,就需要我们在Gallery的Adapter里面,对getView方法,进行改造,从而完成预览小图片的倒影效果。
(3)实现自定义适配器ImageAdapter,完成倒影效果
要完成倒映效果,我们需要在getView方法中,对进来的图片进行处理,具体代码如下:
1 package com.mythou.grallery3d; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 import java.util.Map; 7 import android.content.Context; 8 import android.graphics.Bitmap; 9 import android.graphics.Bitmap.Config; 10 import android.graphics.BitmapFactory; 11 import android.graphics.Canvas; 12 import android.graphics.LinearGradient; 13 import android.graphics.Matrix; 14 import android.graphics.Paint; 15 import android.graphics.PorterDuff.Mode; 16 import android.graphics.PorterDuffXfermode; 17 import android.graphics.Shader.TileMode; 18 import android.view.View; 19 import android.view.ViewGroup; 20 import android.widget.BaseAdapter; 21 import android.widget.ImageView; 22 import android.widget.ImageView.ScaleType; 23 24 /** 25 * 自定义适配器ImageAdapter 26 * 在ImageAdapter中主要做了图片的倒影效果 以及 创建了对原始图片和倒影的显示区域 27 * 28 * @author hebao 29 * 30 */ 31 public class ImageAdapter extends BaseAdapter { 32 private ImageView[] mImages;// 图片数组 33 private Context mContext;// 上下文,提供给外界(Activity)使用 34 public List<Map<String, Object>> list;// 存放图片的集合 35 36 public int[] imgs = { R.drawable.image01, 37 R.drawable.image02, R.drawable.image03, 38 R.drawable.image04, 39 R.drawable.image05 }; 40 41 public String[] titles = { 42 "图片01", "图片02", 43 "图片03", "图片04", 44 "图片05", "图片06", 45 "图片07" 46 }; 47 48 public ImageAdapter(Context c) { 49 this.mContext = c; 50 list = new ArrayList<Map<String, Object>>(); 51 52 for (int i = 0; i < imgs.length; i++) { 53 HashMap<String, Object> map = new HashMap<String, Object>(); 54 map.put("image", imgs[i]); 55 list.add(map); 56 } 57 mImages = new ImageView[list.size()]; 58 } 59 60 /** 61 * 创建倒影效果 62 */ 63 public boolean createReflectedImages() { 64 // 倒影图和原图之间的距离 65 final int reflectionGap = 4; 66 final int Height = 200; 67 int index = 0; 68 for (Map<String, Object> map : list) { 69 Integer id = (Integer) map.get("image"); 70 71 // 返回原图解码之后的bitmap对象 72 Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), id); 73 int width = originalImage.getWidth(); 74 int height = originalImage.getHeight(); 75 float scale = Height / (float) height; 76 77 System.out.println("scale:"+scale); 78 79 // 创建矩阵对象 80 Matrix sMatrix = new Matrix(); 81 // 指定一个角度以0,0为坐标进行旋转 82 // matrix.setRotate(30); 83 84 /** 85 * 缩放图片动作 86 * 第一个参数是x轴的缩放比例,而第二个参数是y轴的缩放比例 87 */ 88 sMatrix.postScale(scale, scale); 89 /** 90 * Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter) 91 * 图片裁剪,可用这个方法: 92 * Bitmap source:要从中截图的原始位图 93 * int x: 起始x坐标 94 * int y: 起始y坐标 95 * int width: 要截的图的宽度 96 * int height:要截的图的高度 97 * boolean filter:参数为true可以进行滤波处理,有助于改善新图像质量;flase时,计算机不做过滤处理。 98 * 99 */ 100 Bitmap miniBitmap = Bitmap.createBitmap(originalImage, 0, 0, originalImage.getWidth(), 101 originalImage.getHeight(), sMatrix, true); 102 //Bitmap内存回收 103 originalImage.recycle(); 104 105 106 int mwidth = miniBitmap.getWidth(); 107 int mheight = miniBitmap.getHeight(); 108 Matrix matrix = new Matrix(); 109 // 指定矩阵(x轴不变,y轴相反) 110 matrix.preScale(1, -1); 111 // 将矩阵应用到该原图之中,返回一个宽度不变,高度为原图1/2的倒影位图 112 Bitmap reflectionImage = Bitmap.createBitmap(miniBitmap, 0, mheight / 2, mwidth, mheight / 2, matrix, 113 false); 114 // 创建一个宽度不变,高度为原图+倒影图高度的位图 115 Bitmap bitmapWithReflection = Bitmap.createBitmap(mwidth, (mheight + mheight / 2), Config.ARGB_8888); 116 // 将上面创建的位图初始化到画布 117 Canvas canvas = new Canvas(bitmapWithReflection); 118 canvas.drawBitmap(miniBitmap, 0, 0, null); 119 120 Paint paint = new Paint(); 121 canvas.drawRect(0, mheight, mwidth, mheight + reflectionGap, paint); 122 canvas.drawBitmap(reflectionImage, 0, mheight + reflectionGap, null); 123 124 paint = new Paint(); 125 /** 126 * 线性渐变 127 * 参数一:为渐变起初点坐标x位置, 128 * 参数二:为y轴位置, 129 * 参数三和四:分辨对应渐变终点, 最后参数为平铺方式, 130 * 这里设置为镜像Gradient是基于Shader类,所以我们通过Paint的setShader方法来设置这个渐变 131 */ 132 LinearGradient shader = new LinearGradient(0, miniBitmap.getHeight(), 0, 133 bitmapWithReflection.getHeight() + reflectionGap, 0x70ffffff, 0x00ffffff, TileMode.CLAMP); 134 // 设置阴影 135 paint.setShader(shader); 136 paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)); 137 // 用已经定义好的画笔构建一个矩形阴影渐变效果 138 canvas.drawRect(0, mheight, mwidth, bitmapWithReflection.getHeight() + reflectionGap, paint); // ���Ƶ�Ӱ����ӰЧ�� 139 140 // 创建一个ImageView用来显示已经画好的bitmapWithReflection 141 ImageView imageView = new ImageView(mContext); 142 imageView.setImageBitmap(bitmapWithReflection); 143 // 设置imageView大小 ,也就是最终显示的图片大小 144 imageView.setLayoutParams( 145 new GalleryView.LayoutParams((int) (width * scale), (int) (mheight * 3 / 2.0 + reflectionGap))); 146 imageView.setScaleType(ScaleType.MATRIX); 147 mImages[index++] = imageView; 148 } 149 return true; 150 } 151 152 @Override 153 public int getCount() { 154 return imgs.length; 155 } 156 157 @Override 158 public Object getItem(int position) { 159 return mImages[position]; 160 } 161 162 @Override 163 public long getItemId(int position) { 164 return position; 165 } 166 167 @Override 168 public View getView(int position, View convertView, ViewGroup parent) { 169 return mImages[position]; 170 } 171 172 public float getScale(boolean focused, int offset) { 173 return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset))); 174 } 175 176 }
(4)使用自定义的Gallery控件实现最终的图片浏览器
首先我们来到grallery_layout.xml布局文件之中,如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="fill_parent" 4 android:layout_height="fill_parent" 5 android:orientation="vertical" > 6 7 <TextView 8 android:id="@+id/tvTitle" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:layout_centerHorizontal="true" 12 android:textSize="16sp" /> 13 14 <com.mythou.grallery3d.GalleryView 15 android:id="@+id/mygallery" 16 android:spacing="20dp" 17 android:layout_width="fill_parent" 18 android:layout_height="fill_parent" 19 android:unselectedAlpha="128" 20 android:layout_below="@id/tvTitle" 21 android:layout_marginTop="10dip" /> 22 23 </RelativeLayout>
接下来我们来到Activity之中,如下:
1 package com.mythou.grallery3d; 2 import android.app.Activity; 3 import android.os.Bundle; 4 import android.view.View; 5 import android.view.Window; 6 import android.widget.AdapterView; 7 import android.widget.AdapterView.OnItemClickListener; 8 import android.widget.AdapterView.OnItemSelectedListener; 9 import android.widget.TextView; 10 import android.widget.Toast; 11 12 public class Grallery3DActivity extends Activity { 13 14 private TextView tvTitle; 15 private GalleryView gallery; 16 private ImageAdapter adapter; 17 18 @Override 19 public void onCreate(Bundle savedInstanceState) { 20 requestWindowFeature(Window.FEATURE_NO_TITLE); 21 super.onCreate(savedInstanceState); 22 setContentView(R.layout.grallery_layout); 23 24 initRes(); 25 } 26 27 private void initRes(){ 28 tvTitle = (TextView) findViewById(R.id.tvTitle); 29 gallery = (GalleryView) findViewById(R.id.mygallery); 30 31 adapter = new ImageAdapter(this); 32 //创建倒影效果 33 adapter.createReflectedImages(); 34 gallery.setAdapter(adapter); 35 36 gallery.setOnItemSelectedListener(new OnItemSelectedListener() { 37 @Override 38 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 39 tvTitle.setText(adapter.titles[position]); 40 } 41 42 @Override 43 public void onNothingSelected(AdapterView<?> parent) { 44 } 45 }); 46 47 gallery.setOnItemClickListener(new OnItemClickListener() { 48 @Override 49 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 50 Toast.makeText(Grallery3DActivity.this, "img " + (position+1) + " selected", Toast.LENGTH_SHORT).show(); 51 } 52 }); 53 } 54 }
(5)布署程序到手机上,效果如下: