优质Android小部件:索尼滚动相册
虽然骚尼手机卖的不怎么样,但是有些东西还是做的挺好的,工业设计就不用说了,索尼的相册的双指任意缩放功能也是尤其炫酷。其桌面小部件滚动相册我觉得也挺好的,比谷歌原生的相册墙功能好多了,网上搜了一下也没发现有人写这个,于是,下面就介绍下我的高A货。
首先是效果图:
主要手势操作有:
- 上/下满速移动,可以上滑/下滑一张图片
- 上/下快读移动,则根据滑动速度,上滑/下滑多张图片
- 单击则请求系统图库展示该图片
该小部件的主要优点:在屏幕内的小范围内提供一个很好的图片选择/浏览部件,尤其是切换图片时有很强的靠近/远离动画感,增加好感。
代码分析
刚开始想这个小部件的时候以为是利用多个ImageView叠加实现的效果,例如谷歌原生的该部件就是利用多个ImageView叠加形成的,但是效果远比不上这个。但觉得通过多个ImageView叠加可能会没这么流畅,性能上也不好。该效果本身也比较规律,应该可以通过一个View来实现,达到更好的性能。于是通过View Hierarchy分析,sony这个果然是通过一个View实现的,于是通过如下方式这个小部件。
代码主要由三个部分组成:
- RollImageView:实际的View
- CellCalculater:用来实时计算每张图片的绘制区域以及透明度,这个是本小部件的核心部件。接口定义如下:
/**
* get all rects for drawing image
* @return
*/
public Cell[] getCells(); /**
*
* @param distance the motion distance during the period from ACTION_DOWN to this moment
* @return 0 means no roll, positive number means roll forward and negative means roll backward
*/
public int setStatus(float distance); /**
* set the dimen of view
* @param widht
* @param height
*/
public void setDimen(int widht, int height); /**
* set to the status for static
*/
public void setStatic();
- ImageLoader:用来加载图片,提供Bitmap给RollImageView绘制。接口定义如下:
/**
* the images shown roll forward
*/
public void rollForward(); /**
* the images shown roll backward
*/
public void rollBackward(); /**
* get bitmaps
* @return
*/
public Bitmap[] getBitmap(); /**
* use invalidate to invalidate the view
* @param invalidate
*/
public void setInvalidate(RollImageView.InvalidateView invalidate); /**
* set the dimen of view
* @param width
* @param height
*/
public void setDimen(int width, int height); /**
* the image path to be show
* @param paths
*/
public void setImagePaths(List<String> paths); /**
* get large bitmap while static
*/
public void loadCurrentLargeBitmap();
下面分析每个部分的核心代码。
RollImageView
View的主要职责是draw各个bitmap以及响应用户的手势操作,相对比较简单。
绘制部分就是把从ImageLoader获得的的各个Bitmap按照从CellCalculater中获得的绘制区域以及透明度绘制到屏幕上,目前本代码实现的比较简单,没有考虑不同尺寸的图片需要进行一些更加协调的显示方式,比如像ImageView.ScaleType中定义的一些显示方式。
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap[] bitmaps = mImageLoader.getBitmap();
Cell[] cells = mCellCalculator.getCells(); //得到每张Image的显示区域与透明度
canvas.translate(getWidth() / 2, 0);
for (int i = SHOW_CNT - 1; i >= 0; i--) { //从最底层的Image开始绘制
Bitmap bitmap = bitmaps[i];
Cell cell = cells[i];
if (bitmap != null && !bitmap.isRecycled()) {
mPaint.setAlpha(cell.getAlpha());
LOG("ondraw " + i + bitmap.getWidth() + " " + cell.getRectF() + " alpha " + cell.getAlpha());
canvas.drawBitmap(bitmap, null, cell.getRectF(), mPaint);
}
}
}
手势部分采用了GestureListener,主要代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getPointerCount() > 1) {
return false;
}
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_UP: //这里主要用于处理没有触发Fling事件时,使界面保持没有移动的状态
if(!mIsFling){
if(mRollResult == CellCalculator.ROLL_FORWARD){
mImageLoader.rollForward();
} else if (mRollResult == CellCalculator.ROLL_BACKWARD && !mScrollRollBack){
mImageLoader.rollBackward();
}
LOG("OnGestureListener ACTION_UP setstatic " );
mCellCalculator.setStatic();
mImageLoader.loadCurrentLargeBitmap();
}
break;
default:
break;
}
return true;
} //缓慢拖动
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mScrollDistance += distanceY;
if(mScrollDistance > 0 && !mScrollRollBack){
mImageLoader.rollBackward();
mScrollRollBack = true;
} else if(mScrollDistance < 0 && mScrollRollBack){
mImageLoader.rollForward();
mScrollRollBack = false;
}
LOG("OnGestureListener onScroll " + distanceY + " all" + mScrollDistance);
mRollResult = mCellCalculator.setStatus(-mScrollDistance);
invalidate();
return true;
} //快速拖动
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (Math.abs(velocityY) > MIN_FLING) {
LOG("OnGestureListener onFling " + velocityY);
if (mExecutorService == null) {
mExecutorService = Executors.newSingleThreadExecutor();
}
mIsFling = true;
mExecutorService.submit(new FlingTask(velocityY));
}
return true;
} //利用一个异步任务来处理滚动多张Images
private class FlingTask implements Runnable { float mVelocity;
float mViewHeight;
int mSleepTime;
boolean mRollBackward; FlingTask(float velocity) {
mRollBackward = velocity < 0 ? true : false;
mVelocity = Math.abs(velocity / 4);
mViewHeight = RollImageView.this.getHeight() / 2;
mSleepTime = (int)(4000 / Math.abs(velocity) * 100); //the slower velocity of fling, the longer interval for roll
} @Override
public void run() {
int i = 0;
try{
while (mVelocity > mViewHeight) {
mCellCalculator.setStatus(mRollBackward ? -mViewHeight : mViewHeight);
mHandler.sendEmptyMessage(MSG_INVALATE);
//determines the count of roll. The using of mViewHeight has no strictly logical
mVelocity -= mViewHeight;
if (((i++) & 1) == 0) { //roll forward once for every two setStatus
if(mRollBackward){
mImageLoader.rollBackward();
}else {
mImageLoader.rollForward();
}
}
Thread.sleep(mSleepTime);
}
mCellCalculator.setStatic();
mImageLoader.loadCurrentLargeBitmap();
mHandler.sendEmptyMessage(MSG_INVALATE);
} catch(Exception e){ } finally{ }
}
}
CellCalculater分析
首先阐明下向前移动/向后移动的概念。需要显示的图片路径存储为一个List,假设显示在最前的图片的索引为index,则当前显示的图片为[index,index+3],向前则表示index加1,向后则表示index减1.
CellCalculater的计算情形主要在于用户通过手势操作,表达了需要向前或者向后移动一张图片的意图。在View中能够获取到的只是手势移动的距离,所以在CellCalculater中需要对传进来的移动距离进行处理,输出移动结果。在我的实现中,当移动距离超过图片高度一半的时候,就表示显示的图片需要移动一位,否则当手势操作结束的时候就设置为static状态。主要代码如下:
public DefaultCellCalculator(int showCnt){
mCnt = showCnt;
mCells = new Cell[mCnt];
mAlphas = new float[mCnt];
STATIC_ALPHA = new int[mCnt];
STATIC_ALPHA[mCnt - 1] = 0; //最后一张图的透明度为0
int alphaUnit = (255 - FIRST_ALPHA) / (mCnt - 2);
for(int i = mCnt - 2; i >= 0; i--){ //定义静态时每张图的透明度
STATIC_ALPHA[i] = FIRST_ALPHA + (mCnt - 2 - i) * alphaUnit;
}
} @Override
public Cell[] getCells() {
return mCells;
} //用户手势移动,distance表示移动距离,正负值分别意味着需要向前/向后移动
@Override
public int setStatus(float distance) {
if(distance > 0){
return calculateForward(distance);
} else if(distance < 0){
return calculateBackward(distance);
} else{
initCells();
}
return 0;
} //设置RollImageView的尺寸,从而计算合适的显示区域
@Override
public void setDimen(int widht, int height) {
mViewWidth = widht;
mViewHeight = height;
mWidhtIndent = (int)(WIDHT_INDENT * mViewWidth);
mWidths = new int[mCnt];
for(int i = 0; i < mCnt; i++){
mWidths[i] = mViewWidth - i * mWidhtIndent;
}
//每张图片的高度。
//假如显示四张图,那么在上面会有三个高度落差,然后最底部保留一个高度落差,所以是mcnt-1
mImageHeight = mViewHeight - (mCnt - 1) * HEIGHT_INDENT;
LOG("mImageHeight " + mImageHeight);
initCells();
} //静态时,即用户手势操作结束时
@Override
public void setStatic() {
initCells();
} //用户有需要向前移动一位的趋势
private int calculateForward(float status){
float scale = status / mImageHeight;
LOG("scale " + scale + " mImageHeight " + mImageHeight + " status " + status);
for(int i = 1; i < mCnt; i++){
mCells[i].setWidth(interpolate(scale * 3, mWidths[i], mWidths[i - 1])); // *3 使得后面的宽度快速增大,经验值
mCells[i].moveVertical(interpolate(scale * 10, 0, HEIGHT_INDENT)); //*10使得后面的图片迅速向前,向前的动画感更强
mCells[i].setAlpha((int)interpolate(scale, STATIC_ALPHA[i], STATIC_ALPHA[i - 1]));
}
mCells[0].moveVertical(status);
mCells[0].setAlpha((int)interpolate(scale, 255, 0));
if(status >= mImageHeight / 3){
return ROLL_FORWARD;
} else {
return 0;
}
} //用户有需要向后移动一位的趋势
private int calculateBackward(float status){
float scale = Math.abs(status / mImageHeight);
for(int i = 1; i < mCnt; i++){
mCells[i].setWidth(interpolate(scale, mWidths[i - 1], mWidths[i]));
mCells[i].moveVertical(-scale * HEIGHT_INDENT);
mCells[i].setAlpha((int)interpolate(scale, STATIC_ALPHA[i - 1], STATIC_ALPHA[i]));
}
mCells[0].resetRect();
mCells[0].setWidth(mWidths[0]);
mCells[0].setHeight(mImageHeight);
mCells[0].moveVertical(mImageHeight + status);
mCells[0].setAlpha((int)interpolate(scale, 0, 255));
if(-status >= mImageHeight / 3){
return ROLL_BACKWARD;
} else {
return 0;
}
} /**
* status without move
*/
private void initCells(){
int top = -HEIGHT_INDENT;
for(int i = 0; i < mCnt; i++){
RectF rectF = new RectF(0,0,0,0);
rectF.top = top + (mCnt - 1 - i) * HEIGHT_INDENT;
rectF.bottom = rectF.top + mImageHeight;
mCells[i] = new Cell(rectF, STATIC_ALPHA[i]);
mCells[i].setWidth(mWidths[i]);
}
} //计算差值
private float interpolate(float scale, float start, float end){
if(scale > 1){
scale = 1;
}
return start + scale * (end - start);
}
ImageLoader分析
ImageLoader其实比较简单,主要有如下两点:
- 响应手势操作,处理对应的向前/向后移动时的Bitmap请求
- 当手势还在操作时,应该加载小图,等手势操作结束之后,应该加载大图。因为只有缓慢移动时,需要清晰显示,而快速移动时,显示小图即可,所以需要加载当前index以及向前向后一张图即可。
//加载当前index以及向前向后三张大图
@Override
public void loadCurrentLargeBitmap() {
for(int i = mCurrentIndex - 1; i < mCurrentIndex + 2; i++){
if(i >= 0 && i < mImagesCnt - 1){
mBitmapCache.getLargeBitmap(mAllImagePaths[i]);
}
}
} //index向前移动一位
@Override
public void rollForward() {
LOG("rollForward");
mCurrentIndex++;
if(mCurrentIndex > mImagesCnt - 1){
mCurrentIndex = mImagesCnt - 1;
}
setCurrentPaths();
} //index向后移动一位
@Override
public void rollBackward() {
LOG("rollBackward");
mCurrentIndex--;
if(mCurrentIndex < 0){
mCurrentIndex = 0;
}
setCurrentPaths();
} @Override
public Bitmap[] getBitmap() {
if(mCurrentPaths != null){
LOG("getBitmap paths nut null");
for(int i = mCurrentIndex, j = 0; j < mShowCnt; j++, i++){
if(i >= 0 && i < mImagesCnt){
mCurrentBitmaps[j] = mBitmapCache.getBimap(mAllImagePaths[i]);
} else{
mCurrentBitmaps[j] = mBitmapCache.getBimap(NO_PATH);
}
}
}
return mCurrentBitmaps;
}
最后,所有源代码:https://github.com/willhua/RollImage
优质Android小部件:索尼滚动相册的更多相关文章
- Android小部件Widget开发过程中的坑和总结
@ 目录 概述 官方参考 效果图 AndroidManifest.xml Receiver Service Options res/xml/ widget_desktop_options.xml 常用 ...
- Android开发5:应用程序窗口小部件App Widgets的实现
前言 本次主要是实现一个Android应用,实现静态广播.动态广播两种改变 widget内容的方法,即在上篇博文中实验的基础上进行修改,所以此次实验的重点是AppWidget小部件的实现啦~ 首先,我 ...
- Android 之窗口小部件详解--App Widget
Android 之窗口小部件详解--App Widget 版本号 说明 作者 日期 1.0 添加App Widge介绍和示例 Sky Wang 2013/06/27 1 App ...
- Android 之窗口小部件详解(三) 部分转载
原文地址:http://blog.csdn.net/iefreer/article/details/4626274. (一) 应用程序窗口小部件App Widgets 应用程序窗口小部件(Widget ...
- Android 之窗口小部件高级篇--App Widget 之 RemoteViews - 跨到对岸去
在之前的一篇博文( Android 之窗口小部件详解--App Widge t)中,已经介绍了App Widget的基本用法和简单实例.这篇主要讲解 App Widget 的高级内容,即通过 Remo ...
- Android Widget(窗口小部件)
Android Widget简介 应用程序窗口小部件(Widget)是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新.你可以通过一个App Widget Provide ...
- Android简易实战教程--第十四话《模仿金山助手创建桌面Widget小部件》
打开谷歌api,对widget小部件做如下说明: App Widgets are miniature application views that can be embedded in otherap ...
- Android 之窗口小部件高级篇--App Widget 之 RemoteViews
Android 之窗口小部件高级篇--App Widget 之 RemoteViews 在之前的一篇博文(Android 之窗口小部件详解--App Widget)中,已经介绍了App Widget的 ...
- 【Android界面实现】AppWidght全面学习之电量监控小部件的实现具体解释
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/bz419927089/article/details/35791047 前几天翻看之前下载的各种资料 ...
随机推荐
- 使用jQuery的animate方法制作滑动菜单
周末看Ziv小威的博客<制作滑动条菜单,如何延时处理滑动效果,避免动画卡顿>,参见地址:http://www.cnblogs.com/zivxiaowei/p/3462964.html.是 ...
- 关于z-index鲜为人知的事情
关于z-index很少有人去深入的了解它,因为它看起来一点儿也不复杂,不就是谁的数字大,谁就显示在前面吗?然而今天所摘录的这篇博文,让我震惊了.我承认我从来没有花时间去看具体的z-index相关文档, ...
- android 如何正确使用 泛型 和 多参数 “偷懒”
我要实现这样一个标题栏 共 4 个选项,采用布局是一个 TextView 对应一个小三角 ImageView,各个选项没被点击时,字体颜色是 黑色,小三角不显示,点击后,字体变色,小三角居下显示,同时 ...
- WebWorker的importScripts方法
简述 在<JavaScript高级程序设计(第三版)>中,提到WebWorker的importScripts方法是异步执行的,然而在 另一本书<Javascript权威指南>中 ...
- angular使用总结
一.是否有必要加入模块化框架 1.Reqruiejs seajs的主要作用 (1)模块化,让代码易于维护. angular本身就是mvc,模块化很清晰,所以这点用不到requirejs (2)可以按需 ...
- mybatis入门基础(九)----逆向工程
一.什么是逆向工程 mybaits需要程序员自己编写sql语句,mybatis官方提供逆向工程 可以针对单表自动生成mybatis执行所需要的代码(mapper.java,mapper.xml.po. ...
- MindMup 是一个开源的、在线的、简单的思维导图工具
MindMup是一个开源.在线的思维导图工具:它有以下特点: 开源 在线 导图可存放在网站(公有,要是在不同的终端浏览的话需要记住导图的网址)或google driver(私有),无用户名密码 很方便 ...
- vue+sass 下sass不能运行问题
好久没写博文了,今天抽空写一写,最近在用vue.js build 项目,今早想使用sass来编译css,可是安装好依赖包之后仍然显示一下错误: ERROR in ENOENT: no such fil ...
- webControls与客户端脚本路径
网上有用的资料不多,在一本电子书中摘抄了内容如下 webControls配置节只有一个clientScriptsLocation属性,此属性用于指定ASP.NET客户端脚本的默认存放路径.这些文件是包 ...
- webapi文档描述-swagger
最近做的项目使用mvc+webapi,采取前后端分离的方式,后台提供API接口给前端开发人员.这个过程中遇到一个问题后台开发人员怎么提供接口说明文档给前端开发人员,最初打算使用word文档方式进行交流 ...