最近研究了一下如何在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,代码如下:

 package pym.test.gallery3d.widget;

 import pym.test.gallery3d.util.BitmapScaleDownUtil;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader.TileMode;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView; /**
* @author pengyiming
* @date 2013-9-30
* @function GalleryFlow适配器
*/
public class ImageAdapter extends BaseAdapter
{
/* 数据段begin */
private final String TAG = "ImageAdapter";
private Context mContext; //图片数组
private int[] mImageIds ;
//图片控件数组
private ImageView[] mImages;
//图片控件LayoutParams
private GalleryFlow.LayoutParams mImagesLayoutParams;
/* 数据段end */ /* 函数段begin */
public ImageAdapter(Context context, int[] imageIds)
{
mContext = context;
mImageIds = imageIds;
mImages = new ImageView[mImageIds.length];
mImagesLayoutParams = new GalleryFlow.LayoutParams(Gallery.LayoutParams.WRAP_CONTENT, Gallery.LayoutParams.WRAP_CONTENT);
} /**
* @function 根据指定宽高创建待绘制的Bitmap,并绘制到ImageView控件上
* @param imageWidth
* @param imageHeight
* @return void
*/
public void createImages(int imageWidth, int imageHeight)
{
// 原图与倒影的间距5px
final int gapHeight = 5; int index = 0;
for (int imageId : mImageIds)
{
/* step1 采样方式解析原图并生成倒影 */
// 解析原图,生成原图Bitmap对象
// Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), imageId);
Bitmap originalImage = BitmapScaleDownUtil.decodeSampledBitmapFromResource(mContext.getResources(), imageId, imageWidth, imageHeight);
int width = originalImage.getWidth();
int height = originalImage.getHeight(); // Y轴方向反向,实质就是X轴翻转
Matrix matrix = new Matrix();
matrix.setScale(1, -1);
// 且仅取原图下半部分创建倒影Bitmap对象
Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, height / 2, width, height / 2, matrix, false); /* step2 绘制 */
// 创建一个可包含原图+间距+倒影的新图Bitmap对象
Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + gapHeight + height / 2), Config.ARGB_8888);
// 在新图Bitmap对象之上创建画布
Canvas canvas = new Canvas(bitmapWithReflection);
// 抗锯齿效果
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG));
// 绘制原图
canvas.drawBitmap(originalImage, 0, 0, null);
// 绘制间距
Paint gapPaint = new Paint();
gapPaint.setColor(0xFFCCCCCC);
canvas.drawRect(0, height, width, height + gapHeight, gapPaint);
// 绘制倒影
canvas.drawBitmap(reflectionImage, 0, height + gapHeight, null); /* step3 渲染 */
// 创建一个线性渐变的渲染器用于渲染倒影
Paint paint = new Paint();
LinearGradient shader = new LinearGradient(0, height, 0, (height + gapHeight + height / 2), 0x70ffffff, 0x00ffffff, TileMode.CLAMP);
// 设置画笔渲染器
paint.setShader(shader);
// 设置图片混合模式
paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));
// 渲染倒影+间距
canvas.drawRect(0, height, width, (height + gapHeight + height / 2), paint); /* step4 在ImageView控件上绘制 */
ImageView imageView = new ImageView(mContext);
imageView.setImageBitmap(bitmapWithReflection);
imageView.setLayoutParams(mImagesLayoutParams);
// 打log
imageView.setTag(index); /* step5 释放heap */
originalImage.recycle();
reflectionImage.recycle();
// bitmapWithReflection.recycle(); mImages[index++] = imageView;
}
} @Override
public int getCount()
{
return Integer.MAX_VALUE;
} @Override
public Object getItem(int position)
{
return mImages[position];
} @Override
public long getItemId(int position)
{
return position;
} @Override
public View getView(int position, View convertView, ViewGroup parent)
{
return mImages[position % mImages.length];
}
/* 函数段end */
}

  其次是GalleryFlow,代码如下:

 package pym.test.gallery3d.widget;

 import android.content.Context;
import android.graphics.Camera;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Transformation;
import android.widget.Gallery; /**
* @author pengyiming
* @date 2013-9-30
* @function 自定义控件
*/
public class GalleryFlow extends Gallery
{
/* 数据段begin */
private final String TAG = "GalleryFlow"; // 边缘图片最大旋转角度
private final float MAX_ROTATION_ANGLE = 75;
// 中心图片最大前置距离
private final float MAX_TRANSLATE_DISTANCE = -100;
// GalleryFlow中心X坐标
private int mGalleryFlowCenterX;
// 3D变换Camera
private Camera mCamera = new Camera();
/* 数据段end */ /* 函数段begin */
public GalleryFlow(Context context, AttributeSet attrs)
{
super(context, attrs); // 开启,在滑动过程中,回调getChildStaticTransformation()
this.setStaticTransformationsEnabled(true);
} /**
* @function 获取GalleryFlow中心X坐标
* @return
*/
private int getCenterXOfCoverflow()
{
return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft();
} /**
* @function 获取GalleryFlow子view的中心X坐标
* @param childView
* @return
*/
private int getCenterXOfView(View childView)
{
return childView.getLeft() + childView.getWidth() / 2;
} /**
* @note step1 系统调用measure()方法时,回调此方法;表明此时系统正在计算view的大小
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec); mGalleryFlowCenterX = getCenterXOfCoverflow();
Log.d(TAG, "onMeasure, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
} /**
* @note step2 系统调用layout()方法时,回调此方法;表明此时系统正在给child view分配空间
* @note 必定在onMeasure()之后回调,但与onSizeChanged()先后顺序不一定
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b); mGalleryFlowCenterX = getCenterXOfCoverflow();
Log.d(TAG, "onLayout, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
} /**
* @note step2 系统调用measure()方法后,当需要绘制此view时,回调此方法;表明此时系统已计算完view的大小
* @note 必定在onMeasure()之后回调,但与onSizeChanged()先后顺序不一定
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh); mGalleryFlowCenterX = getCenterXOfCoverflow();
Log.d(TAG, "onSizeChanged, mGalleryFlowCenterX = " + mGalleryFlowCenterX);
} @Override
protected boolean getChildStaticTransformation(View childView, Transformation t)
{
// 计算旋转角度
float rotationAngle = calculateRotationAngle(childView); // 计算前置距离
float translateDistance = calculateTranslateDistance(childView); // 开始3D变换
transformChildView(childView, t, rotationAngle, translateDistance); return true;
} /**
* @function 计算GalleryFlow子view的旋转角度
* @note1 位于Gallery中心的图片不旋转
* @note2 位于Gallery中心两侧的图片按照离中心点的距离旋转
* @param childView
* @return
*/
private float calculateRotationAngle(View childView)
{
final int childCenterX = getCenterXOfView(childView);
float rotationAngle = 0; rotationAngle = (mGalleryFlowCenterX - childCenterX) / (float) mGalleryFlowCenterX * MAX_ROTATION_ANGLE; if (rotationAngle > MAX_ROTATION_ANGLE)
{
rotationAngle = MAX_ROTATION_ANGLE;
}
else if (rotationAngle < -MAX_ROTATION_ANGLE)
{
rotationAngle = -MAX_ROTATION_ANGLE;
} return rotationAngle;
} /**
* @function 计算GalleryFlow子view的前置距离
* @note1 位于Gallery中心的图片前置
* @note2 位于Gallery中心两侧的图片不前置
* @param childView
* @return
*/
private float calculateTranslateDistance(View childView)
{
final int childCenterX = getCenterXOfView(childView);
float translateDistance = 0; if (mGalleryFlowCenterX == childCenterX)
{
translateDistance = MAX_TRANSLATE_DISTANCE;
} return translateDistance;
} /**
* @function 开始变换GalleryFlow子view
* @param childView
* @param t
* @param rotationAngle
* @param translateDistance
*/
private void transformChildView(View childView, Transformation t, float rotationAngle, float translateDistance)
{
t.clear();
t.setTransformationType(Transformation.TYPE_MATRIX); final Matrix imageMatrix = t.getMatrix();
final int imageWidth = childView.getWidth();
final int imageHeight = childView.getHeight(); mCamera.save(); /* rotateY */
// 在Y轴上旋转,位于中心的图片不旋转,中心两侧的图片竖向向里或向外翻转。
mCamera.rotateY(rotationAngle);
/* rotateY */ /* translateZ */
// 在Z轴上前置,位于中心的图片会有放大的效果
mCamera.translate(0, 0, translateDistance);
/* translateZ */ // 开始变换(我的理解是:移动Camera,在2D视图上产生3D效果)
mCamera.getMatrix(imageMatrix);
imageMatrix.preTranslate(-imageWidth / 2, -imageHeight / 2);
imageMatrix.postTranslate(imageWidth / 2, imageHeight / 2); mCamera.restore();
}
/* 函数段end */
}

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

 package pym.test.gallery3d.util;

 import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.Display; /**
* @author pengyiming
* @date 2013-9-30
* @function Bitmap缩放处理工具类
*/
public class BitmapScaleDownUtil
{
/* 数据段begin */
private final String TAG = "BitmapScaleDownUtil";
/* 数据段end */ /* 函数段begin */
/**
* @function 获取屏幕大小
* @param display
* @return 屏幕宽高
*/
public static int[] getScreenDimension(Display display)
{
int[] dimension = new int[2];
dimension[0] = display.getWidth();
dimension[1] = display.getHeight(); return dimension;
} /**
* @function 以取样方式加载Bitmap
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return 取样后的Bitmap
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
{
// step1,将inJustDecodeBounds置为true,以解析Bitmap真实尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options); // step2,计算Bitmap取样比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // step3,将inJustDecodeBounds置为false,以取样比列解析Bitmap
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
} /**
* @function 计算Bitmap取样比例
* @param options
* @param reqWidth
* @param reqHeight
* @return 取样比例
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// 默认取样比例为1:1
int inSampleSize = 1; // Bitmap原始尺寸
final int width = options.outWidth;
final int height = options.outHeight; // 取最大取样比例
if (height > reqHeight || width > reqWidth)
{
final int widthRatio = Math.round((float) width / (float) reqWidth);
final int heightRatio = Math.round((float) height / (float) reqHeight); // 取样比例为X:1,其中X>=1
inSampleSize = Math.max(widthRatio, heightRatio);
} return inSampleSize;
}
/* 函数段end */
}

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

 package pym.test.gallery3d.main;

 import pym.test.gallery3d.R;
import pym.test.gallery3d.util.BitmapScaleDownUtil;
import pym.test.gallery3d.widget.GalleryFlow;
import pym.test.gallery3d.widget.ImageAdapter;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle; /**
* @author pengyiming
* @date 2013-9-30
*/
public class Gallery3DActivity extends Activity
{
/* 数据段begin */
private final String TAG = "Gallery3DActivity";
private Context mContext; // 图片缩放倍率(相对屏幕尺寸的缩小倍率)
public static final int SCALE_FACTOR = 8; // 图片间距(控制各图片之间的距离)
private final int GALLERY_SPACING = -10; // 控件
private GalleryFlow mGalleryFlow;
/* 数据段end */ /* 函数段begin */
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mContext = getApplicationContext(); setContentView(R.layout.gallery_3d_activity_layout);
initGallery();
} private void initGallery()
{
// 图片ID
int[] images = {
R.drawable.picture_1,
R.drawable.picture_2,
R.drawable.picture_3,
R.drawable.picture_4,
R.drawable.picture_5,
R.drawable.picture_6,
R.drawable.picture_7 }; ImageAdapter adapter = new ImageAdapter(mContext, images);
// 计算图片的宽高
int[] dimension = BitmapScaleDownUtil.getScreenDimension(getWindowManager().getDefaultDisplay());
int imageWidth = dimension[0] / SCALE_FACTOR;
int imageHeight = dimension[1] / SCALE_FACTOR;
// 初始化图片
adapter.createImages(imageWidth, imageHeight); // 设置Adapter,显示位置位于控件中间,这样使得左右均可"无限"滑动
mGalleryFlow = (GalleryFlow) findViewById(R.id.gallery_flow);
mGalleryFlow.setSpacing(GALLERY_SPACING);
mGalleryFlow.setAdapter(adapter);
mGalleryFlow.setSelection(Integer.MAX_VALUE / 2);
}
/* 函数段end */
}

  see效果图~~~

Android改进版CoverFlow效果控件的更多相关文章

  1. Android Material适配 为控件设置指定背景色和点击波纹效果

    Android Material适配 为控件设置指定背景色和点击波纹效果,有需要的朋友可以参考下. 大部分时候,我们都需要为控件设置指定背景色和点击效果 4.x以下可以使用selector,5.0以上 ...

  2. Android开发:文本控件详解——TextView(二)文字跑马灯效果实现

    一.需要使用的属性: 1.android:ellipsize 作用:若文字过长,控制该控件如何显示. 对于同样的文字“Android开发:文本控件详解——TextView(二)文字跑马灯效果实现”,不 ...

  3. android中的EditView控件

    android中的EditView控件 EditText继承关系:View-->TextView-->EditText ,EditText是可编辑文本框 1.EditText默认情况下,光 ...

  4. Android中通过WebView控件实现与JavaScript方法相互调用的地图应用

    在Android中通过WebView控件,可以实现要加载的页面与Android方法相互调用,我们要实现WebView中的addJavascriptInterface方法,这样html才能调用andro ...

  5. Android 性能优化——之控件的优化

    Android 性能优化——之控件的优化 前面讲了图像的优化,接下来分享一下控件的性能优化,这里主要是面向自定义View的优化. 1.首先先说一下我们在自定义View中可能会犯的3个错误: 1)Use ...

  6. Android 5.0新控件——FloatingActionButton(悬浮按钮)

    Android 5.0新控件--FloatingActionButton(悬浮按钮) FloatingActionButton是5.0以后的新控件,一个悬浮按钮,之所以叫做悬浮按钮,主要是因为自带阴影 ...

  7. Android开发:文本控件详解——TextView(一)基本属性

    一.简单实例: 新建的Android项目初始自带的Hello World!其实就是一个TextView. 在activity_main.xml中可以新建TextView,从左侧组件里拖拽到右侧预览界面 ...

  8. Android高级_视频播放控件

    一.Android系统自带VideoView控件 1. 创建步骤: (1)自带视频文件放入res/raw文件夹下: (2)声明初始化VideoView控件: (3)创建视频文件Uri路径,Uri调用p ...

  9. Android仿iPhone 滚轮控件 实现

    Android_开发 实用滚轮效果选择数字http://blog.csdn.net/zhangtengyuan23/article/details/8653771 Android仿iPhone滚轮控件 ...

随机推荐

  1. Golang Context 详细介绍

    Golang context 本文包含对context实现上的分析和使用方式,分析部分源码讲解比价多,可能会比较枯燥,读者可以直接跳过去阅读使用部分. ps: 作者本着开源分享的精神撰写本篇文章,如果 ...

  2. (三)Hyperledger Fabric 1.1安装部署-chaincode测试

    环境搭建完毕,需要的工具和镜像安装完毕,就可以进行chaincode测试了,接下来参考官方教程运行first-network. 进入first-netwok: cd first-network fir ...

  3. PAT甲题题解-1108. Finding Average (20)-字符串处理

    求给出数的平均数,当然有些是不符合格式的,要输出该数不是合法的. 这里我写了函数来判断是否符合题目要求的数字,有点麻烦. #include <iostream> #include < ...

  4. 数据库——SQL数据连接查询

    连接查询 查询结果或条件涉及多个表的查询称为连接查询SQL中连接查询的主要类型     广义笛卡尔积     等值连接(含自然连接)     自身连接查询     外连接查询 一.广义笛卡尔积 不带连 ...

  5. 关于Python matplotlib显示汉字乱码问题

    我也是一个初学者,在今天编程时遇到的一个问题,我是基于Eclipse编写Python代码,在使用matplotlib进行数据可视化时,发现显示不了汉字并且出现乱码问题. (1)使用中文注释时报错: 解 ...

  6. JS基础(五)自定义函数

    作用:是为了让重复使用的语句,方便进行调用. 定义格式: function 自定义函数名 (参数1, 参数2,...) { 执行的语句 } 函数的封装:把语句放到函数中去的过程. 参数:通过参数的改变 ...

  7. Java之扫描目录,修改文件内容

    扫描目录下文件,修改文件中指定内容 package org.utils.tools.fileoper; import java.io.*; import java.util.ArrayList; im ...

  8. LOJ2542 PKUWC2018随机游走(概率期望+容斥原理)

    如果直接dp,状态里肯定要带上已走过的点的集合,感觉上不太好做. 考虑一种对期望的minmax容斥:其中Max(S)为遍历完S集合的期望步数,Min(S)为遍历到S集合中一个点的期望步数.当然才不管怎 ...

  9. photoshop cc 2018破解补丁(pscc2018注册机) 附使用方法

    1.下载破解程序 破解文件自行到  http://www.ddooo.com/softdown/109954.htm 下载 博主可以到本博客的文件-->pscc2018zcj_109954.ra ...

  10. 【题解】 bzoj1088: [SCOI2005]扫雷Mine (神奇的做法)

    bzoj1088,懒得复制,戳我戳我 Solution: 其实这个有个结论,答案只会有\(0\),\(1\),\(2\)三种(我真的是个弱鸡,这个都想不到) 然后我们假设第一个就可以推出所有的状态(显 ...