我要介绍的是一个

能旋转的view,说这个view能旋转有点不切实际,那是视觉效果,其实是对图片的旋转。目前它只支持图片。你可以把它认为是一个能响应手势旋转的View。

它的功能有:

1.会响应手势旋转

2.该view模拟真实罗盘旋转:a.旋转的时候会有惯性,继续旋转,而且是减速旋转b.旋转期间手指扳动罗盘,能加速罗盘旋转c.当罗盘在旋转的时候,手指按住罗盘,它会有刹车的效果。

效果截图:

为了形象点我用了一张风车的图作为例子

android 静态图标实现旋转 android图片旋转动画_ide

技术要点

1.需要扩展一个view,重写ondraw(),onTouchEvent(),onMeasure(),onDetachedFromWindow()方法

a.onDraw():主要是控制图片旋转绘图

b.onTouchEvent():主要是监听手势

c.onMeasere():用来测量view的长宽,在xml里最好配置成wrap_content,因为如果为固定值可能会因为长宽不够,导致显示不全

d.onDetachedFromWindow():用来回收bitmap

2.需要通过handler来处理惯性

3.需要一个速度分析器,来分析手势离开时的瞬时速度

4.需要用到圆和三角函数的知识:如反正切函数,弧度等

技术难点分析

1.如何扩展这个View

a.View的旋转图片的设置

我们可以提供一个方法来设置旋转的图片,并定义旋转图片的成员变量,这里我将它命名为rotaBitmap

public void setRotatBitmap(Bitmap bitmap) {
rotatBitmap = bitmap;
initSize();
postInvalidate();
}
public void setRotatDrawableResource(int id) {
BitmapDrawable drawable = (BitmapDrawable)getContext().getResources().getDrawable(id);
setRotatDrawable(drawable);
}
public void setRotatDrawable(BitmapDrawable drawable) {
rotatBitmap = drawable.getBitmap();
initSize();
postInvalidate();
}

b.View长宽确认

有了图片就可以确认这个view的大小了,这里view的大小不是image的大小,因为还要考虑旋转,一个矩形要确保旋转360度都能被看见,那这个区域应该是个正方形,而且这个正方形的内切圆半径是这个矩形的对角线一半。不知道能否说明白,还是我画图吧。

android 静态图标实现旋转 android图片旋转动画_android 静态图标实现旋转_02

通过上图,应该很容易发现,这个view的长度应该是被旋转图的对角线的长度

这样我们可以加上这样一段代码:

private void initSize() {
if (rotatBitmap == null) {
// throw new NoBitMapError("Error,No bitmap in RotatView!");
return;
}
width = rotatBitmap.getWidth();
height = rotatBitmap.getHeight();
maxwidth = Math.sqrt(width * width + height * height);
o_x = o_y = (float)(maxwidth / 2);//确定圆心坐标
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 它的宽高不是图片的宽高,而是以宽高为直角的矩形的对角线的长度
setMeasuredDimension((int)maxwidth, (int)maxwidth);
}

c.旋转原理

图片的旋转是在ondraw()里实现的,通过一个变量:deta_degree

来控制旋转的度数

/**

* 当前圆盘所转的弧度(以该 view 的中心为圆点)

*/

float deta_degree;

然后用Matrix来控制旋转图片,主要是preRotate(deta_degree)这里的单位是度,360度为一圈,最后把旋转的图画到画布上

@Override
protected void onDraw(Canvas canvas) {
Matrix matrix = new Matrix();
// 设置转轴位置
matrix.setTranslate((float)width / 2, (float)height / 2);
// 开始转
matrix.preRotate(deta_degree);
// 转轴还原
matrix.preTranslate(-(float)width / 2, -(float)height / 2);
// 将位置送到view的中心
matrix.postTranslate((float)(maxwidth - width) / 2, (float)(maxwidth - height) / 2);
canvas.drawBitmap(rotatBitmap, matrix,paint);
super.onDraw(canvas);
}

考虑到它的周期为360,如果detaDegree的度数太大可能会越界,我们可以做一个于求余处理,让它的值在-360到360之间

/**
* 通过此方法来控制旋转度数,如果超过360,让它求余,防止,该值过大造成越界
*
* @param added
*/
private void addDegree(float added) {
deta_degree += added;
if (deta_degree > 360 || deta_degree < -360) {
deta_degree = deta_degree % 360;
}
}

这里的动画是通过不停的走ondraw()方法刷新,产生的效果,类似放电影一样

d.view的手势响应

当用户触摸它时会响应onTouch事件,在onTouch里分析手指的坐标,通过坐标算出与圆心的夹角

下图为手指与view中心的夹角(这里的原点就是view的旋转中心):

android 静态图标实现旋转 android图片旋转动画_Math_03

每次手指滑动或是松开都会计算它与原点的夹角,这个方法可以通过反正切函数求出来,详情:

/**
* 计算以(src_x,src_y)为坐标圆点,建立直角体系,求出(target_x,target_y)坐标与x轴的夹角
* 主要是利用反正切函数的知识求出夹角
*
* @param src_x
* @param src_y
* @param target_x
* @param target_y
* @return
*/
float detaDegree(float src_x, float src_y, float target_x, float target_y) {
float detaX = target_x - src_x;
float detaY = target_y - src_y;
double d;
//坐标在四个象限里
if (detaX != 0) {
float tan = Math.abs(detaY / detaX);
if (detaX > 0) {
//第一象限
if (detaY >= 0) {
d = Math.atan(tan);
} else {
//第四象限
d = 2 * Math.PI - Math.atan(tan);
}
} else {
if (detaY >= 0) {
//第二象限
d = Math.PI - Math.atan(tan);
} else {
//第三象限
d = Math.PI + Math.atan(tan);
}
}
} else {
//坐标在y轴上
if (detaY > 0) {
//坐标在y>0上
d = Math.PI / 2;
} else {
//坐标在y<0上
d = -Math.PI / 2;
}
}
return (float)((d * 180) / Math.PI);
}

通过以上方法,可以把每次移动的时候的夹角求出来,把当前的夹角和上次的手指夹角坐差运算就能求出手指相对圆心旋转的角度增量,得到这个角度增量就可以通过调用

这前提过的addDegree()方法,改变图片的角度,然后调用invalidate()方法重绘,就实现了罗盘随手指旋转的效果。

在这里赋上ontouch处理down和move事件的代码(up事件是用来处理惯性用的):

@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
if (rotatBitmap == null) {
throw new NoBitMapError("Error,No bitmap in RotatView!");
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
down_x = event.getX();
down_y = event.getY();
current_degree = detaDegree(o_x, o_y, down_x, down_y);
break;
}
case MotionEvent.ACTION_MOVE: {
down_x = target_x = event.getX();
down_y = target_y = event.getY();
float degree = detaDegree(o_x, o_y, target_x, target_y);
// 滑过的弧度增量
float dete = degree - current_degree;
// 如果小于-90度说明 它跨周了,需要特殊处理350->17,
if (dete < -270) {
dete = dete + 360;
// 如果大于90度说明 它跨周了,需要特殊处理-350->-17,
} else if (dete > 270) {
dete = dete - 360;
}
addDegree(dete);
current_degree = degree;
invalidate();
break;
}

e.View的瞬时速度获取

为了得到瞬时速度,我的思路是通过一个固定长度的2维数组,把手指与原点的最近几次夹角增量和时间点记录下来,通过这几个夹角增量和时间点,可以算出平均速度,因为手指滑动的时候,响应ontouch事件次数非常多,我们可以把最后几次ontouch记录下的数据,取平均值,把它认为是瞬时速度。

这里用到了一点物理知识:

android 静态图标实现旋转 android图片旋转动画_System_04

假如上图为record记录的4组数据,t代表时间点,表示产生这个事件的时间,d代表手指与圆心夹角的增量,它是这次夹角与上次夹角的差值

这样我们可以把t=t3-t0算出经过的时间,把sum=d1+d2+d3算出这段时间一共经历过的弧度

再把sum/t就是平均速度了

但需要注意一个细节:d0是无效的

这里给出计算速度 的代码:

因为考虑不能让速度太快,所以给出了一个最大值

/**
* 最大速度
*/
public static final double max_speed = 8;
/**
* 通过数组里所装载的数据分析出即时速度
* 原理是:计算数组里的时间长度和增量的总数,然后求出每毫秒所走过的弧度
* 当然不能超过{@link VRecord#max_speed}
*
* @return
*/
public double getSpeed() {
if (addCount == 0) {
return 0;
}
int maxIndex = Math.min(addCount, length) - 1;
if ((record[0][1] - record[maxIndex][1]) == 0) {
return 0;
}
double detaTime = record[0][1] - record[maxIndex][1];
double sumdegree = 0;
for (int i = 0; i < length - 1; i++) {
sumdegree += record[0];
// System.out.println(record[0]);
}
// System.out.println("----------");
// System.out.println(sumdegree);
// System.out.println(detaTime);
double result = sumdegree / detaTime;
if (result > 0) {
return Math.min(result, max_speed);
} else {
return Math.max(result, -max_speed);
}
// System.out.println("v=" + result);
}

讲到这我要提一下,这个二维数组是如何做到获取最近的数据,如果超过容量,它将把原来的数据丢弃,我想直接上代码,大家就能看懂吧

/**
* 二维数组,1.保存弧度增量.2.保存产生这个增量的时间点
*/
double[][] record = new double[length][2];
/**
* 为二维数组装载数据
* 注:通过此方法,有个特点,能把最后的length组数据记录下来,length以外的会丢失
*
* @param detadegree
* @param time
*/
public void add(double detadegree, double time) {
for (int i = length - 1; i > 0; i--) {
record[0] = record[i - 1][0];
record[1] = record[i - 1][1];
}
record[0][0] = detadegree;
record[0][1] = time;
addCount++;
}

f.View惯性处理

这里的惯性处理就是用到加速度了,再用handler发消息,控制它不停减速,减到它为零为止就停止发消息

下面是handler代码

@Override
public void handleMessage(Message msg) {
double detaTime = System.currentTimeMillis() - currentTime;
switch (msg.what) {
case play: {
//如果是顺时针
if (isClockWise) {
speed = speed - a * detaTime;//减速
if (speed <= 0) {
return;
} else {
handler.sendEmptyMessageDelayed(play, delayedTime);
}
} else {
speed = speed + a * detaTime;
if (speed >= 0) {
return;
} else {
handler.sendEmptyMessageDelayed(play, delayedTime);
}
}
addDegree((float)(speed * detaTime + (a * detaTime * detaTime) / 2));//高中物理算路程S=vt+at2
// if (a < a_max) {
// a = (float)(a + a_add*detaTime);
// System.out.println("a:"+a);
// }
currentTime = System.currentTimeMillis();
invalidate();
break;
}
case stop: {
speed = 0;
handler.removeMessages(play);
}
}
super.handleMessage(msg);
}
};