目錄
效果展示
前言
●之前有一個(gè)項(xiàng)目需要用到轉(zhuǎn)盤效果,但是那個(gè)時(shí)候思路不是很清晰,所以引用了一下別人的控件但是別人的源碼還是比較復(fù)雜的午阵,后來經(jīng)過分析后使用一種簡(jiǎn)單的方式寫出了之前的效果。
●如果有小伙伴想實(shí)現(xiàn)九宮格抽獎(jiǎng)效果的話請(qǐng)看我的另一篇文章《Android超簡(jiǎn)單實(shí)現(xiàn)九宮格抽獎(jiǎng)》
實(shí)現(xiàn)步驟
1.畫轉(zhuǎn)盤
-
實(shí)現(xiàn)原理
- 對(duì)應(yīng)代碼
public class LuckPan extends View {
private Paint mPaintArc;//轉(zhuǎn)盤扇形畫筆
private float mRadius;//圓盤的半徑
private RectF rectFPan;//構(gòu)建轉(zhuǎn)盤的矩形
private String[] mItemStrs = {"俯臥撐30個(gè)","波比跳15個(gè)","卷腹30個(gè)","高抬腿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)點(diǎn)作為圓心
rectFPan = new RectF(-mRadius,-mRadius,mRadius,mRadius);
//每一個(gè)Item的角度
mItemAnge = 360 / mItemStrs.length;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//為了操作方便將畫布中心點(diǎn)設(shè)置為(0窘拯,0)
drawPanItem(canvas);//畫轉(zhuǎn)盤
}
private void drawPanItem(Canvas canvas) {
float startAng = 0;//扇形開始的角度
for (int x = 1;x<= mItemStrs.length;x++){
//這里我們通過判斷奇數(shù)偶數(shù)來給轉(zhuǎn)盤設(shè)置不同的顏色
if(x%2 == 1){
//是奇數(shù)
mPaintArc.setColor(Color.WHITE);
}else {
//偶數(shù)
mPaintArc.setColor(Color.parseColor("#F8864A"));
}
canvas.drawArc(rectFPan,startAng,mItemAnge,true,mPaintArc);//畫扇形
startAng+=mItemAnge;//每畫完一次增加開始角度
}
}
}
- 對(duì)應(yīng)效果
2.生成畫文字路徑 -
實(shí)現(xiàn)原理
- 對(duì)應(yīng)代碼
在代碼中我們需要添加幾個(gè)屬性。(其中為了展示方便省略了部分代碼)
public class LuckPan extends View {
//...省略部分
private Paint mPaintItemStr;//轉(zhuǎn)盤文字畫筆
private ArrayList<Path> mArcPaths;
private RectF rectFStr;//構(gòu)建文字圓盤的矩形
private float mTextSize = 20;//文字大小
private String[] mItemStrs = {"俯臥撐30個(gè)","波比跳15個(gè)","卷腹30個(gè)","高抬腿30下","深蹲30下","開合跳30下"};//繪制的文字?jǐn)?shù)組
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"));//設(shè)置文字顏色
mPaintItemStr.setStrokeWidth(3);
mPaintItemStr.setTextAlign(Paint.Align.CENTER);//設(shè)置文字水平居中對(duì)齊
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);//構(gòu)建文字路徑的矩形半徑為圓盤的五分之七
mTextSize = mRadius/9;//根據(jù)圓盤的半徑設(shè)置文字大小
mPaintItemStr.setTextSize(mTextSize);//設(shè)置文字大小
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//畫布中心點(diǎn)設(shè)置為(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){
//是奇數(shù)
mPaintArc.setColor(Color.WHITE);
}else {
//偶數(shù)
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;
}
}
}
-
對(duì)應(yīng)效果
3.設(shè)置動(dòng)畫實(shí)現(xiàn)轉(zhuǎn)動(dòng)
-
實(shí)現(xiàn)原理
- 對(duì)應(yīng)代碼
這里為了看的清楚也是省略了部分代碼涤姊。
public class LuckPan extends View {
//省略部分代碼....
private int mRepeatCount = 4;//轉(zhuǎn)幾圈
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();
}
}
- 對(duì)應(yīng)效果
4.糾正初始角度
通過上面的代碼我們已經(jīng)基本實(shí)現(xiàn)了轉(zhuǎn)盤效果了,但是我們發(fā)現(xiàn)轉(zhuǎn)盤初始的角度不是在第一個(gè)Item上嗤放。 -
調(diào)整原理
- 對(duì)應(yīng)代碼
為了觀看方便這里只展示onDraw里的代碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//畫布中心點(diǎn)設(shè)置為(0思喊,0)
canvas.rotate(-90);//將畫布旋轉(zhuǎn)-90度
drawPanItem(canvas);
drawText(canvas);
}
-
對(duì)應(yīng)效果
這里我們發(fā)現(xiàn)在旋轉(zhuǎn)了-90度后雖然指向了第一個(gè)Item但是卻沒有指到扇形的中間,因此我們需要繼續(xù)旋轉(zhuǎn)畫布為-ItemAngle/2的度數(shù)即代碼實(shí)現(xiàn)為:
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;//設(shè)置偏移角度
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth()/2,getHeight()/2);//畫布中心點(diǎn)設(shè)置為(0次酌,0)
canvas.rotate(-90-mOffsetAngle);//將畫布旋轉(zhuǎn)(-90-mOffsetAngle)度
drawPanItem(canvas);
drawText(canvas);
}
}
- 對(duì)應(yīng)效果
5.糾正下一次旋轉(zhuǎn)位置為上一次結(jié)束位置以及糾正結(jié)束位置
我們雖然糾正了初始位置但是我們發(fā)現(xiàn)轉(zhuǎn)盤的下一次旋轉(zhuǎn)角度卻不是上一次結(jié)束的位置恨课,而且停止的位置也不正確,下面我們就來糾正下這兩個(gè)問題岳服。 - 實(shí)現(xiàn)代碼
同樣為了方便觀看這里只展示部分代碼剂公。
public class LuckPan extends View {
private float mStartAngle = 0;//存儲(chǔ)圓盤開始的位置
private ObjectAnimator objectAnimator;
public void startAnim(){
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum-mStartAngle%360;//如果轉(zhuǎn)過一次了那下次旋轉(zhuǎn)的角度就需要減去上一次多出的,否則結(jié)束的位置會(huì)不斷增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle+mRepeatCount*360+v);
objectAnimator.setDuration(4000);
objectAnimator.start();
mStartAngle += mRepeatCount*360+v;//將上一次的角度加進(jìn)來以達(dá)到下次開始就是上次結(jié)束的位置
}
}
-
對(duì)應(yīng)效果
我們發(fā)現(xiàn)雖然第二次開始的位置是上一次結(jié)束的位置吊宋,但是最終停止的位置卻不對(duì)诬留,因?yàn)槲覀冊(cè)O(shè)置的是2,而停止的卻是倒數(shù)第2個(gè),其實(shí)這主要是因?yàn)檗D(zhuǎn)盤是順時(shí)針旋轉(zhuǎn)的原因文兑,在這里有兩種解決方案:
1.將結(jié)束位置設(shè)置為負(fù)數(shù):(這種方法就不展示代碼了盒刚,直接將mLuckNum設(shè)置為負(fù)數(shù)即可)
2.將旋轉(zhuǎn)角度變?yōu)槟鏁r(shí)針:(就是將旋轉(zhuǎn)角度變?yōu)樨?fù)數(shù))
代碼展示:
//這里其實(shí)就是將角度計(jì)算那部分+變成-
public void startAnim(){
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum+mStartAngle%360;//如果轉(zhuǎn)過一次了那下次旋轉(zhuǎn)的角度就需要減去上一次多出的,否則結(jié)束的位置會(huì)不斷增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle-mRepeatCount*360-v);
objectAnimator.setDuration(4000);
objectAnimator.start();
mStartAngle -= mRepeatCount*360+v;
}
- 對(duì)應(yīng)效果
6.添加動(dòng)態(tài)設(shè)置參數(shù)的方法
就是動(dòng)態(tài)的設(shè)置Item所顯示的內(nèi)容绿贞,及停止的位置因块。 - 對(duì)應(yīng)代碼
/**
* 設(shè)置轉(zhuǎn)盤數(shù)據(jù)
* @param items
*/
public void setItems(String[] items){
mItemStrs = items;
mOffsetAngle=0;
mStartAngle=0;
mOffsetAngle = 360/items.length/2;//根據(jù)item的數(shù)量動(dòng)態(tài)調(diào)整圓盤偏移角度
invalidate();
}
/**
* 設(shè)置轉(zhuǎn)盤數(shù)據(jù)
*/
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","哥哥哥","對(duì)應(yīng)效果","對(duì)應(yīng)代碼"};
@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();
}
});
}
}
- 對(duì)應(yīng)效果
7.添加結(jié)束回調(diào)函數(shù) - 對(duì)應(yīng)代碼
回調(diào)接口代碼:
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);//隨機(jī)生成結(jié)束位置
if(objectAnimator!=null){
objectAnimator.cancel();
}
float v = mItemAnge*mLuckNum+mStartAngle%360;//如果轉(zhuǎn)過一次了那下次旋轉(zhuǎn)的角度就需要減去上一次多出的,否則結(jié)束的位置會(huì)不斷增加的
objectAnimator = ObjectAnimator.ofFloat(this, "rotation", mStartAngle, mStartAngle-mRepeatCount*360-v);
objectAnimator.setDuration(4000);
//在動(dòng)畫的監(jiān)聽事件中增加我們實(shí)現(xiàn)的回調(diào)函數(shù)
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","哥哥哥","對(duì)應(yīng)效果","對(duì)應(yīng)代碼"};
@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();
}
});
}
}
-
對(duì)應(yīng)效果