Android 抽奖转盘的实现
Posted Nipuream
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 抽奖转盘的实现相关的知识,希望对你有一定的参考价值。
** 本篇文章已授权公众号 guolin_blog (郭霖)独家发布 **
序言
最近需要实现一个抽奖的控件,我简单搜索了下,感觉要不很多细节地方没有处理,要么,根本就不能用。索性想自己实现个,从千图网搜了下,挑选了个自己比较喜欢的出来,psd打开后效果如下:
最终实现效果如下:
点击Go按钮自动滚动:
随手势滚动:
实现的效果还不错,因为是模拟器加录制,画面可能会有些卡顿,真机其实蛮顺畅的,下面简单的讲讲实现的步骤。
实现
1,绘制。
首先第一个我们要它给画出来,但是要注意的就是android所对应的坐标系的问题。
for(int i= 0;i<6;i++)
if(i%2 == 0)
canvas.drawArc(rectF,angle,60,true,dPaint);
else
canvas.drawArc(rectF,angle,60,true,sPaint);
angle += 60;
for(int i=0;i<6;i++)
drawIcon(width/2, height/2, radius, InitAngle, i, canvas);
InitAngle += 60;
for(int i=0;i<6;i++)
drawText(InitAngle+30,strs[i], 2*radius, textPaint, canvas,rectF);
InitAngle += 60;
其中有两个地方需要注意下,第一个就是画弧的地方第一个角度是起始角度,第二个是弧的角度,并不是结束的角度,所以是固定值60。第二个地方就是计算具体的x,y的值的时候要根据弧度去计算,不能根据角度。
- 2.使用属性动画让其自动旋转。
如果用SurfaceView去进行重绘旋转存在一些问题,比如旋转的角度不好控制,旋转的速度不好控制。但是用属性动画,这个问题就很好解决了。
ValueAnimator animtor = ValueAnimator.ofInt(InitAngle,DesRotate);
animtor.setInterpolator(new AccelerateDecelerateInterpolator());
animtor.setDuration(time);
animtor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
@Override
public void onAnimationUpdate(ValueAnimator animation)
int updateValue = (int) animation.getAnimatedValue();
InitAngle = (updateValue % 360 + 360) % 360;
ViewCompat.postInvalidateOnAnimation(RotatePan.this);
);
animtor.addListener(new AnimatorListenerAdapter()
@Override
public void onAnimationEnd(Animator animation)
super.onAnimationEnd(animation);
int pos = InitAngle / 60;
if(pos >= 0 && pos <= 3)
pos = 3 - pos;
else
pos = (6-pos) + 3;
if(l != null)
l.endAnimation(pos);
);
animtor.start();
用动画最重要的就是,如何计算出结束动画后的位置,那么把最终旋转的总角度%360°就得到最后一圈实际旋转的角度,再除以60就得到了到底选择了几个位置,因为一个位置占据60°,这应该不难理解。
@Override
public void onAnimationEnd(Animation animation)
int pos = startDegree % 360 / 60;
if(pos >= 0 && pos <= 3)
pos = 3 - pos;
else
pos = (6-pos) + 3;
if(l != null)
l.endAnimation(pos);
但是问题又来了,Android所对应的坐标系,0的位置应该是最底下,而指针的位置是在最上面,所以,我们结合上面的坐标系来看,还需要处理下,如上面的代码所示。
3.利用Scroller和GestureDetector对手势进行处理。
触摸事件的处理,最后到底允不允许转盘随手势滑动呢?其实貌似做成这样也就可以了,但是最后还是实现了下,用到了GestureDetector 和 Scroller这个类。其实做法有很多,首先获取我们的滑动的距离,Math.sqrt(dx * dx + dy * dy),然后无非就是把这个距离转换成我们需要的角度,你可以把这个距离当作我们的周长来处理,也可以把这个距离当作我们总的旋转的角度来处理。之后就是随着时间的流逝,不断的刷新我们的界面了。
@Override
public boolean onTouchEvent(MotionEvent event)
boolean consume = mDetector.onTouchEvent(event);
if(consume)
getParent().requestDisallowInterceptTouchEvent(true);
return true;
return super.onTouchEvent(event);
public void setRotate(int rotation)
rotation = (rotation % 360 + 360) % 360;
InitAngle = rotation;
ViewCompat.postInvalidateOnAnimation(this);
@Override
public void computeScroll()
if(scroller.computeScrollOffset())
setRotate(scroller.getCurrY());
super.computeScroll();
private class RotatePanGestureListener extends GestureDetector.SimpleOnGestureListener
@Override
public boolean onDown(MotionEvent e)
return super.onDown(e);
@Override
public boolean onDoubleTap(MotionEvent e)
return false;
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
float centerX = (RotatePan.this.getLeft() + RotatePan.this.getRight())*0.5f;
float centerY = (RotatePan.this.getTop() + RotatePan.this.getBottom())*0.5f;
float scrollTheta = vectorToScalarScroll(distanceX, distanceY, e2.getX() - centerX, e2.getY() -
centerY);
int rotate = InitAngle -
(int) scrollTheta / FLING_VELOCITY_DOWNSCALE;
setRotate(rotate);
return true;
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
float centerX = (RotatePan.this.getLeft() + RotatePan.this.getRight())*0.5f;
float centerY = (RotatePan.this.getTop() + RotatePan.this.getBottom())*0.5f;
float scrollTheta = vectorToScalarScroll(velocityX, velocityY, e2.getX() - centerX, e2.getY() -
centerY);
scroller.abortAnimation();
scroller.fling(0, InitAngle , 0, (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
return true;
private float vectorToScalarScroll(float dx, float dy, float x, float y)
// get the length of the vector
float l = (float) Math.sqrt(dx * dx + dy * dy);
// decide if the scalar should be negative or positive by finding
// the dot product of the vector perpendicular to (x,y).
float crossX = -y;
float crossY = x;
float dot = (crossX * dx + crossY * dy);
float sign = Math.signum(dot);
return l * sign;
4.剩余问题处理
还存在个问题,如果没有手势去操作转盘,那我们很容易判断它所旋转的角度,但是有手势的参与,我们很容易旋转到转盘中两个分片中间的位置,那么,我们在让它旋转之前,要简单处理下,避免这种事情发生。
//TODO 为了每次都能旋转到转盘的中间位置
int offRotate = DesRotate % 360 % 60;
DesRotate -= offRotate;
DesRotate += 30;
这样不管手势怎么操作,我最终都是旋转到分片的中间位置了。
- 5.转动到指定某个区域
/**
* 开始转动
* @param pos 如果 pos = -1 则随机,如果指定某个值,则转到某个指定区域
*/
public void startRotate(int pos)
int lap = (int) (Math.random()*12) + 4;
int angle = 0;
if(pos < 0)
angle = (int) (Math.random() * 360);
else
int initPos = queryPosition();
if(pos > initPos)
angle = (pos - initPos)*60;
lap -= 1;
angle = 360 - angle;
else if(pos < initPos)
angle = (initPos - pos)*60;
else
//nothing to do.
好多人加我QQ,问我怎么转动到指定位置,所以更新了下代码,上传到github上去了,这里做个日志。如果传的是 -1 则随机转动,如果传的是大于0,则转动到指定位置。
- 6.改变转盘数量
<com.hr.nipuream.luckpan.view.RotatePan
android:id="@+id/rotatePan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="78dp"
android:layout_centerHorizontal="true"
luckpan:pannum="8"
luckpan:names="@array/names"
luckpan:icons="@array/icons"
/>
<resources>
<string-array name="names">
<item>action</item>
<item>adventure</item>
<item>combat</item>
<item>moba</item>
<item>other</item>
<item>role</item>
<item>sports</item>
<item>words</item>
</string-array>
<string-array name="icons">
<item>action</item>
<item>adventure</item>
<item>combat</item>
<item>moba</item>
<item>other</item>
<item>role</item>
<item>sports</item>
<item>words</item>
</string-array>
</resources>
将pannum改为你想要的数量,然后names和icons定义在arrays.xml文件中, 其中arrays.xml中的数量要和转盘的数量一致。理论上可以改为转盘数量为N的情况,但是综合来看还是6个和8个转盘数量最适宜,而且很多人也只问我怎么改成8个转盘,所以对这两种情况做了适配,如果后期还有别的需求在加吧,github上代码已经更新,请重新下载。
代码
最后,代码已经上传到github上去了。地址:https://github.com/Nipuream/LuckPan 欢迎Star
LuckPan.apk apk下载地址
以上是关于Android 抽奖转盘的实现的主要内容,如果未能解决你的问题,请参考以下文章