目录
效果展示
前言
●之前有一个项目需要用到转盘效果,但是那个时候思路不是很清晰,所以引用了一下别人的控件但是别人的源码还是比较复杂的,后来经过分析后使用一种简单的方式写出了之前的效果。
●如果有小伙伴想实现九宫格抽奖效果的话请看我的另一篇文章《Android超简单实现九宫格抽奖》
实现步骤
1.画转盘
-
实现原理
- 对应代码
public class LuckPan extends View {
private Paint mPaintArc;//转盘扇形画笔
private float mRadius;//圆盘的半径
private RectF rectFPan;//构建转盘的矩形
private String[] mItemStrs = {"俯卧撑30个","波比跳15个","卷腹30个","高抬腿30下","深蹲30下","开合跳30下"};
private float mItemAnge;
public LuckPan(Context context) {
this(context,null);
}
public LuckPan(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public LuckPan(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaintArc = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintArc.setStyle(Paint.Style.FILL);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = Math.min(w,h)/2*0.9f;
//这里是将(0,0)点作为圆心
rectFPan = new RectF(-mRadius,-mRadius,mRadius,mRadius);
//每一个Item的角度
mItemAnge = 360 / mItemStrs.length;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//为了操作方便将画布中心点设置为(0,0)
drawPanItem(canvas);//画转盘
}
private void drawPanItem(Canvas canvas) {
float startAng = 0;//扇形开始的角度
for (int x = 1;x<= mItemStrs.length;x++){
//这里我们通过判断奇数偶数来给转盘设置不同的颜色
if(x%2 == 1){
//是奇数
mPaintArc.setColor(Color.WHITE);
}else {
//偶数
mPaintArc.setColor(Color.parseColor("#F8864A"));
}
canvas.drawArc(rectFPan,startAng,mItemAnge,true,mPaintArc);//画扇形
startAng+=mItemAnge;//每画完一次增加开始角度
}
}
}
- 对应效果
2.生成画文字路径 -
实现原理
- 对应代码
在代码中我们需要添加几个属性。(其中为了展示方便省略了部分代码)
public class LuckPan extends View {
//...省略部分
private Paint mPaintItemStr;//转盘文字画笔
private ArrayList<Path> mArcPaths;
private RectF rectFStr;//构建文字圆盘的矩形
private float mTextSize = 20;//文字大小
private String[] mItemStrs = {"俯卧撑30个","波比跳15个","卷腹30个","高抬腿30下","深蹲30下","开合跳30下"};//绘制的文字数组
public LuckPan(Context context) {
this(context,null);
}
public LuckPan(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public LuckPan(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//...省略部分代码
mPaintItemStr = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintItemStr.setColor(Color.parseColor("#ED2F2F"));//设置文字颜色
mPaintItemStr.setStrokeWidth(3);
mPaintItemStr.setTextAlign(Paint.Align.CENTER);//设置文字水平居中对齐
mArcPaths = new ArrayList<>();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
rectFStr = new RectF(-mRadius/7*5,-mRadius/7*5,mRadius/7*5,mRadius/7*5);//构建文字路径的矩形半径为圆盘的五分之七
mTextSize = mRadius/9;//根据圆盘的半径设置文字大小
mPaintItemStr.setTextSize(mTextSize);//设置文字大小
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//画布中心点设置为(0,0)
drawPanItem(canvas);
drawText(canvas);
}
//画文字
private void drawText(Canvas canvas) {
for(int x = 0;x<mItemStrs.length;x++){
Path path = mArcPaths.get(x);
canvas.drawTextOnPath(mItemStrs[x],path,0,0,mPaintItemStr);
}
}
private void drawPanItem(Canvas canvas) {
float startAng = 0;//扇形开始的角度
for (int x = 1;x<= mItemStrs.length;x++){
if(x%2 == 1){
//是奇数
mPaintArc.setColor(Color.WHITE);
}else {
//偶数
mPaintArc.setColor(Color.parseColor("#F8864A"));
}
//以下是添加文字绘制路径的代码
Path path = new Path();
path.addArc(rectFStr,startAng,mItemAnge);//文字的路径圆形比盘的小
mArcPaths.add(path);
//==========================
canvas.drawArc(rectFPan,startAng,mItemAnge,true,mPaintArc);
startAng+=mItemAnge;
}
}
}
-
对应效果
3.设置动画实现转动
-
实现原理
- 对应代码
这里为了看的清楚也是省略了部分代码。
public class LuckPan extends View {
//省略部分代码....
private int mRepeatCount = 4;//转几圈
private int mLuckNum = 2;//最终停止的位置
private ObjectAnimator objectAnimator;
public LuckPan(Context context) {
this(context,null);
}
public LuckPan(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public LuckPan(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//省略部分代码....
public void startAnim(){
if(objectAnimator!=null){
objectAnimator.cancel();
}
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", 0, mRepeatCount*360+mLuckNum*mItemAnge);
objectAnimator.setDuration(4000);
objectAnimator.start();
}
}
- 对应效果
4.纠正初始角度
通过上面的代码我们已经基本实现了转盘效果了,但是我们发现转盘初始的角度不是在第一个Item上。 -
调整原理
- 对应代码
为了观看方便这里只展示onDraw里的代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//画布中心点设置为(0,0)
canvas.rotate(-90);//将画布旋转-90度
drawPanItem(canvas);
drawText(canvas);
}
-
对应效果
这里我们发现在旋转了-90度后虽然指向了第一个Item但是却没有指到扇形的中间,因此我们需要继续旋转画布为-ItemAngle/2的度数即代码实现为:
public class LuckPan extends View {
private float mOffsetAngle = 0;//圆盘偏移角度(我们需要添加此属性)
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mItemAnge = 360 / mItemStrs.length;
mOffsetAngle = mItemAnge/2;//设置偏移角度
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//画布中心点设置为(0,0)
canvas.rotate(-90-mOffsetAngle);//将画布旋转(-90-mOffsetAngle)度
drawPanItem(canvas);
drawText(canvas);
}
}
- 对应效果
5.纠正下一次旋转位置为上一次结束位置以及纠正结束位置
我们虽然纠正了初始位置但是我们发现转盘的下一次旋转角度却不是上一次结束的位置,而且停止的位置也不正确,下面我们就来纠正下这两个问题。 - 实现代码
同样为了方便观看这里只展示部分代码。
public class LuckPan extends View {
private float mStartAngle = 0;//存储圆盘开始的位置
private ObjectAnimator objectAnimator;
public void startAnim(){
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum-mStartAngle%360;//如果转过一次了那下次旋转的角度就需要减去上一次多出的,否则结束的位置会不断增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle+mRepeatCount*360+v);
objectAnimator.setDuration(4000);
objectAnimator.start();
mStartAngle += mRepeatCount*360+v;//将上一次的角度加进来以达到下次开始就是上次结束的位置
}
}
-
对应效果
我们发现虽然第二次开始的位置是上一次结束的位置,但是最终停止的位置却不对,因为我们设置的是2,而停止的却是倒数第2个,其实这主要是因为转盘是顺时针旋转的原因,在这里有两种解决方案:
1.将结束位置设置为负数:(这种方法就不展示代码了,直接将mLuckNum设置为负数即可)
2.将旋转角度变为逆时针:(就是将旋转角度变为负数)
代码展示:
//这里其实就是将角度计算那部分+变成-
public void startAnim(){
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum+mStartAngle%360;//如果转过一次了那下次旋转的角度就需要减去上一次多出的,否则结束的位置会不断增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle-mRepeatCount*360-v);
objectAnimator.setDuration(4000);
objectAnimator.start();
mStartAngle -= mRepeatCount*360+v;
}
- 对应效果
6.添加动态设置参数的方法
就是动态的设置Item所显示的内容,及停止的位置。 - 对应代码
/**
* 设置转盘数据
* @param items
*/
public void setItems(String[] items){
mItemStrs = items;
mOffsetAngle=0;
mStartAngle=0;
mOffsetAngle = 360/items.length/2;//根据item的数量动态调整圆盘偏移角度
invalidate();
}
/**
* 设置转盘数据
*/
public void setLuckNumber(int luckNumber){
mLuckNum = luckNumber;
}
布局代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.itfitness.luckpan.MainActivity">
<com.itfitness.luckpan.widget.LuckPan
android:id="@+id/pan"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/img_start"
android:src="@mipmap/ic_luckdrawstart"
android:layout_centerInParent="true"
android:layout_width="130dp"
android:layout_height="130dp" />
</RelativeLayout>
Avtivity代码:
public class MainActivity extends AppCompatActivity {
private LuckPan pan;
private ImageView imgStart;
private String[] mItemStrs = {"123","撒大声道1","撒大声道2","撒旦说","撒大声道3","哥哥哥","对应效果","对应代码"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pan = (LuckPan) findViewById(R.id.pan);
imgStart = (ImageView) findViewById(R.id.img_start);
pan.setItems(mItemStrs);
pan.setLuckNumber(2);
imgStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pan.startAnim();
}
});
}
}
- 对应效果
7.添加结束回调函数 - 对应代码
回调接口代码:
public interface LuckPanAnimEndCallBack {
void onAnimEnd(String str);
}
LuckPan代码:这里为了方便省略部分代码
public class LuckPan extends View {
private ObjectAnimator objectAnimator;
private LuckPanAnimEndCallBack luckPanAnimEndCallBack;
public LuckPanAnimEndCallBack getLuckPanAnimEndCallBack() {
return luckPanAnimEndCallBack;
}
public void setLuckPanAnimEndCallBack(LuckPanAnimEndCallBack luckPanAnimEndCallBack) {
this.luckPanAnimEndCallBack = luckPanAnimEndCallBack;
}
public void startAnim(){
// mLuckNum = random.nextInt( mItemStrs.length);//随机生成结束位置
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum+mStartAngle%360;//如果转过一次了那下次旋转的角度就需要减去上一次多出的,否则结束的位置会不断增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle-mRepeatCount*360-v);
objectAnimator.setDuration(4000);
//在动画的监听事件中增加我们实现的回调函数
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if(luckPanAnimEndCallBack!=null){
luckPanAnimEndCallBack.onAnimEnd(mItemStrs[mLuckNum]);
}
}
});
objectAnimator.start();
mStartAngle -= mRepeatCount*360+v;
}
}
Activity代码:
public class MainActivity extends AppCompatActivity {
private LuckPan pan;
private ImageView imgStart;
private String[] mItemStrs = {"123","撒大声道1","撒大声道2","撒旦说","撒大声道3","哥哥哥","对应效果","对应代码"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pan = (LuckPan) findViewById(R.id.pan);
imgStart = (ImageView) findViewById(R.id.img_start);
pan.setItems(mItemStrs);
pan.setLuckNumber(2);
pan.setLuckPanAnimEndCallBack(new LuckPanAnimEndCallBack() {
@Override
public void onAnimEnd(String str) {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
});
imgStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pan.startAnim();
}
});
}
}
-
对应效果