最近在項目中要實作一個九宮格抽獎view 。中間是抽獎按鈕,八個格子是獎品。效果圖如下:
九宮格抽獎View
接下來我就分析一下實作這個View的步驟:
1.繪制出外框(此處難點是繪制閃光點的效果);
2.繪制九個格子,這個就是計算均分的邏輯,比較簡單。
3.實作抽獎動效,以及點選中間start按鈕有個縮放效果的實作。
我一一分析一下。
1.繪制外邊框:
見代碼:核心是使用 canvas.drawRoundRect(rectF, radiusBg, radiusBg, bgPaint);方法繪制圓角邊框矩形,繪制内外兩個邊框矩形,重疊在一起(此處起初想使用畫筆畫邊框,但實作起來隻能内邊框才有圓角,外邊框是直角)。
接下來就是繪制四個角上的小原點(原點是圖檔),這樣做是保證四個角的圖檔一緻,此處邏輯就是計算四個角上的位置稍微麻煩點;然後計算四條邊上的點。最後,使用postDelayed重複繪制,達到閃爍的效果。
public class LuckyDrawLayout extends RelativeLayout {
private static final StringTAG ="LuckyDrawLayout";
private Paint bgPaint;
private int mWidth, mHeight;
private int radiusBg;
private Rect FrectF =new RectF();
private Bitmap smallGreenBitmap;
private Bitmap smallRedBitmap;
private int ballWidth, ballHeight;
private int redBallWidth, redBallHeight;
private RectF ballRectf;
private int innerPadding =dip2px(15);
private boolean isChanged =true;
private int eachRow =13;
public LuckyDrawLayout(Context context) {
this(context, null);
}
public LuckyDrawLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LuckyDrawLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
bgPaint =new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setStrokeCap(Paint.Cap.ROUND);
bgPaint.setStrokeJoin(Paint.Join.ROUND);
bgPaint.setAntiAlias(true);
bgPaint.setDither(true);
bgPaint.setColor(0xFFFF356B);
smallGreenBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_small_green);
smallRedBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_small_red);
ballWidth =smallGreenBitmap.getWidth();
ballHeight =smallGreenBitmap.getHeight();
redBallWidth =smallRedBitmap.getWidth();
redBallHeight =smallRedBitmap.getHeight();
ballRectf =new RectF();
setWillNotDraw(false);
changeBall();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth = MeasureSpec.getSize(widthMeasureSpec);
int mHeight = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(mWidth, mHeight);
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();
mHeight = getHeight();
radiusBg =mWidth /40;
rectF.set(0, 0, mWidth, mHeight);
bgPaint.setColor(0xFFFF356B);
canvas.drawRoundRect(rectF, radiusBg, radiusBg, bgPaint);
rectF.set(innerPadding, innerPadding, mWidth -innerPadding, mHeight -innerPadding);
bgPaint.setColor(0xFFCE0037);
canvas.drawRoundRect(rectF, radiusBg, radiusBg, bgPaint);
drawFourCorner(canvas);
int ballGapDp = (mWidth -innerPadding *2) /eachRow;
for (int i =0; i
if (getGreen(i)) {
//上
ballRectf.set(innerPadding *2 + i * ballGapDp -ballWidth /2, innerPadding /2 -ballHeight /2, innerPadding *2 +ballWidth /2 + i * ballGapDp, innerPadding /2 +ballHeight /2);
canvas.drawBitmap(smallGreenBitmap, null, ballRectf, null);
//下
ballRectf.set(innerPadding *2 + i * ballGapDp -ballWidth /2, mHeight - (innerPadding /2 +ballHeight /2), innerPadding *2 +ballWidth /2 + i * ballGapDp, mHeight - (innerPadding /2 -ballHeight /2));
canvas.drawBitmap(smallGreenBitmap, null, ballRectf, null);
//左
ballRectf.set(innerPadding /2 -ballWidth /2, innerPadding *2 + i * ballGapDp -ballHeight /2, innerPadding /2 +ballWidth /2, innerPadding *2 + i * ballGapDp +ballHeight /2);
canvas.drawBitmap(smallGreenBitmap, null, ballRectf, null);
//右
ballRectf.set(mWidth - (innerPadding /2 +ballWidth /2), innerPadding *2 + i * ballGapDp -ballHeight /2, mWidth - (innerPadding /2 -ballWidth /2), innerPadding *2 + i * ballGapDp +ballHeight /2);
canvas.drawBitmap(smallGreenBitmap, null, ballRectf, null);
}else {
ballRectf.set(innerPadding *2 + i * ballGapDp -redBallWidth /2, innerPadding /2 -redBallHeight /2, innerPadding *2 +redBallWidth /2 + i * ballGapDp, innerPadding /2 +redBallHeight /2);
canvas.drawBitmap(smallRedBitmap, null, ballRectf, null);
ballRectf.set(innerPadding *2 + i * ballGapDp -redBallWidth /2, mHeight - (innerPadding /2 +redBallHeight /2), innerPadding *2 +redBallWidth /2 + i * ballGapDp, mHeight - (innerPadding /2 -redBallHeight /2));
canvas.drawBitmap(smallRedBitmap, null, ballRectf, null);
ballRectf.set(innerPadding /2 -redBallWidth /2, innerPadding *2 + i * ballGapDp -redBallHeight /2, innerPadding /2 +redBallWidth /2, innerPadding *2 + i * ballGapDp +redBallHeight /2);
canvas.drawBitmap(smallRedBitmap, null, ballRectf, null);
ballRectf.set(mWidth - (innerPadding /2 +redBallWidth /2), innerPadding *2 + i * ballGapDp -redBallHeight /2, mWidth - (innerPadding /2 -redBallWidth /2), innerPadding *2 + i * ballGapDp +redBallHeight /2);
canvas.drawBitmap(smallRedBitmap, null, ballRectf, null);
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
for (int i =0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (childinstanceof LuckyDrawView) {
child.layout(innerPadding, innerPadding, getWidth() -innerPadding, getHeight() -innerPadding);
}
}
}
private void changeBall() {
postDelayed(new Runnable() {
@Override
public void run() {
isChanged = !isChanged;
Log.d(TAG, "run: changeBall()");
invalidate();
postDelayed(this, 300);
}
}, 300);
}
private boolean getGreen(int i) {
if (isChanged) {
return i %2 !=0;
}else {
return i %2 ==0;
}
}
private void drawFourCorner(Canvas canvas) {
if (isChanged) {
RectF leftTopRectf =new RectF(innerPadding /2 -ballWidth /2, innerPadding /2 -ballHeight /2, innerPadding /2 +ballWidth /2, innerPadding /2 +ballHeight /2);
canvas.drawBitmap(smallGreenBitmap, null, leftTopRectf, null);
RectF rightTopRectf =new RectF(mWidth - (innerPadding /2 +ballWidth /2), innerPadding /2 -ballHeight /2, mWidth - (innerPadding /2 -ballWidth /2), innerPadding /2 +ballHeight /2);
canvas.drawBitmap(smallGreenBitmap, null, rightTopRectf, null);
RectF leftBottomRectf =new RectF(innerPadding /2 -ballWidth /2, mHeight - (innerPadding /2 +ballHeight /2), innerPadding /2 +ballWidth /2, mHeight - (innerPadding /2 -ballHeight /2));
canvas.drawBitmap(smallGreenBitmap, null, leftBottomRectf, null);
RectF rightBottomRectf =new RectF(mWidth - (innerPadding /2 +ballWidth /2), mHeight - (innerPadding /2 +ballHeight /2), mWidth - (innerPadding /2 -ballWidth /2), mHeight - (innerPadding /2 -ballHeight /2));
canvas.drawBitmap(smallGreenBitmap, null, rightBottomRectf, null);
}else {
RectF leftTopRectf =new RectF(innerPadding /2 -redBallWidth /2, innerPadding /2 -redBallHeight /2, innerPadding /2 +redBallWidth /2, innerPadding /2 +redBallHeight /2);
canvas.drawBitmap(smallRedBitmap, null, leftTopRectf, null);
RectF rightTopRectf =new RectF(mWidth - (innerPadding /2 +redBallWidth /2), innerPadding /2 -redBallHeight /2, mWidth - (innerPadding /2 -redBallWidth /2), innerPadding /2 +redBallHeight /2);
canvas.drawBitmap(smallRedBitmap, null, rightTopRectf, null);
RectF leftBottomRectf =new RectF(innerPadding /2 -redBallWidth /2, mHeight - (innerPadding /2 +redBallHeight /2), innerPadding /2 +redBallWidth /2, mHeight - (innerPadding /2 -redBallHeight /2));
canvas.drawBitmap(smallRedBitmap, null, leftBottomRectf, null);
RectF rightBottomRectf =new RectF(mWidth - (innerPadding /2 +redBallWidth /2), mHeight - (innerPadding /2 +redBallHeight /2), mWidth - (innerPadding /2 -redBallWidth /2), mHeight - (innerPadding /2 -redBallHeight /2));
canvas.drawBitmap(smallRedBitmap, null, rightBottomRectf, null);
}
}
public static int dip2px(float dipValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dipValue * scale +0.5f);
}
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale +0.5f);
}
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale +0.5f);
}
}
2.繪制九宮格:
先見代碼:主要就是計算每個格子的寬高,要均分,針對中間的格子做特殊處理,具體邏輯計算看下面drawNineCell 方法;然後繪制文本,居中顯示即可。接下來,實作轉動的邏輯,private int[]positions = {0, 1, 2, 5, 8, 7, 6, 3}; //順時針 這個記錄了轉動的順序,然後用變量currentPosition 記錄目前的位置下标,通過positions[currentPosition]取出對應的格子,然後繪制該格子的顯示樣式。開啟了一個線程計算currentPosition的值,為了實作一個快要中獎停頓的效果,線上程中轉動最後一圈的時候,使用SystemClock.sleep(100 * (currentPosition +1));讓子線程睡眠時間遞增,currentLoopCount記錄轉動的圈數,預設4圈;stopPosition停止的位置。最後實作點選中間按鈕縮放的效果,先計算中間按鈕的矩形位置mCenterButtonRectF,設定onTouchListener事件,計算點選的區域是否是在mCenterButtonRectF中,mCenterButtonRectF.contains(x, y)。如果在該區域中就執行縮放效果。具體看代碼。
public class LuckyDrawView extends View {
private static final StringTAG ="LuckyDrawView";
//0->1->2->3->5->6->7->8
//0-1-2-5-8-7-6-3
private int currentPosition =0;
private int stopPosition = -1;
private final static int LOOP_COUNT =4;
private int currentLoopCount =0;
private Paint bgPaint;
private int mWidth, mHeight;
private int radiusBg;
private Paint cellPaint;
private Paint cellTextPaint;
private int innerEachGap =dip2px(6);
private int innerWidth, innerHeight;
private int eachWidth, eachHeight;
private boolean onTouchCenter =false;
private RectFmCenterButtonRectF;
private String[]rewardTexts = {"$0.04", "$0.10", "$0.80", "$0.85", "", "$3.00", "$5.00", "$0.15", "$0.10"};
private int[]positions = {0, 1, 2, 5, 8, 7, 6, 3}; //順時針
String start ="Start";
float scale =1.0f;
private boolean isRuning =false;
public LuckyDrawView(Context context) {
this(context, null);
}
public LuckyDrawView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LuckyDrawView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
bgPaint =new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setStrokeCap(Paint.Cap.ROUND);
bgPaint.setStrokeJoin(Paint.Join.ROUND);
bgPaint.setAntiAlias(true);
bgPaint.setDither(true);
bgPaint.setColor(0xFFFF356B);
cellPaint =new Paint(Paint.ANTI_ALIAS_FLAG);
cellPaint.setStyle(Paint.Style.FILL);
cellPaint.setColor(Color.WHITE);
cellTextPaint =new Paint(Paint.ANTI_ALIAS_FLAG);
cellTextPaint.setTextSize(sp2px(context, 26));
cellTextPaint.setColor(Color.WHITE);
cellTextPaint.setTypeface(Typeface.DEFAULT_BOLD);
cellTextPaint.setAntiAlias(true);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onTouchCenter =false;
int x = (int) event.getX();
int y = (int) event.getY();
if (mCenterButtonRectF.contains(x, y) && !isRuning) {
if (scale !=0.8f) {
scale =0.8f;
invalidate();
}
onTouchCenter =true;
}
break;
case MotionEvent.ACTION_UP:
if (onTouchCenter) {
startPressScaleAnim();
startLoop();
}
onTouchCenter =false;
break;
}
return true;
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();
mHeight = getHeight();
radiusBg =mWidth /40;
innerWidth =mWidth -innerEachGap *4;
innerHeight =mHeight -innerEachGap *4;
eachWidth =innerWidth /3;
eachHeight =innerHeight /3;
drawNineCell(canvas);
}
private void drawNineCell(Canvas canvas) {
int nums =9;
RectF rectF =new RectF();
for (int i =0; i < nums; i++) {
int startX =innerEachGap + (i %3) * (eachWidth +innerEachGap);
int startY =innerEachGap + (i /3) * (eachHeight +innerEachGap);
rectF.set(startX, startY, startX +eachWidth, startY +eachHeight);
if (i == nums /2) {
cellPaint.setColor(0xFFFFE535);
bgPaint.setColor(0xFFFF356B);
rectF.set(rectF.left + rectF.left * (1 -scale) *0.08f, rectF.top + rectF.top * (1 -scale) *0.08f, rectF.right - rectF.right * (1 -scale) *0.08f, rectF.bottom - rectF.bottom * (1 -scale) *0.08f);
canvas.drawRoundRect(rectF, radiusBg, radiusBg, cellPaint);
mCenterButtonRectF =new RectF(rectF);
rectF.set(rectF.left +dip2px(10), rectF.top +dip2px(10), rectF.right -dip2px(10), rectF.bottom -dip2px(10));
canvas.drawRoundRect(rectF, radiusBg, radiusBg, bgPaint);
cellTextPaint.setColor(Color.WHITE);
canvas.drawText(start, rectF.centerX() -cellTextPaint.measureText(start) /2, rectF.centerY() + getTextDiffY(cellTextPaint), cellTextPaint);
}else {
if (positions[currentPosition] == i) {
cellPaint.setColor(0xFFFBC01B);
cellTextPaint.setColor(Color.WHITE);
}else {
cellPaint.setColor(Color.WHITE);
cellTextPaint.setColor(0xFFFF5A00);
}
canvas.drawRoundRect(rectF, radiusBg, radiusBg, cellPaint);
canvas.drawText(rewardTexts[i], rectF.centerX() -cellTextPaint.measureText(rewardTexts[i]) /2, rectF.centerY() + getTextDiffY(cellTextPaint), cellTextPaint);
}
}
}
private float getTextDiffY(Paint paint) {
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
return Math.abs(fontMetrics.descent - fontMetrics.ascent) /2 - fontMetrics.descent;
}
private void startLoop() {
currentLoopCount =0;
Random random =new Random();
stopPosition = random.nextInt(7);
currentPosition =0;
new Thread(action).start();
}
private Runnableaction =new Runnable() {
@Override
public void run() {
while (true) {
isRuning =true;
if (currentLoopCount >=LOOP_COUNT) {
isRuning =false;
postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(getContext(), "恭喜你抽中了position=" +stopPosition +"(" +rewardTexts[positions[stopPosition]] +")", Toast.LENGTH_LONG).show();
}
}, 500);
break;
}
currentPosition++;
if (currentPosition >7) {
currentLoopCount++;
currentPosition =0;
}
post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
if (currentLoopCount ==LOOP_COUNT -1) {
if (currentPosition %7 ==stopPosition) {
if (currentPosition ==stopPosition) {
currentLoopCount =LOOP_COUNT;
}
}
SystemClock.sleep(100 * (currentPosition +1));
}else {
SystemClock.sleep(100);
}
}
}
};
private void startPressScaleAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.8f, 1.0f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scale = ((float) animation.getAnimatedValue());
invalidate();
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
}
public static int dip2px(float dipValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dipValue * scale +0.5f);
}
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale +0.5f);
}
}
不足之處:
1.繪制了兩層外框,中間的紅色區域繪制了兩次,導緻過渡繪制了。起初的想法是用bgPaint.setStyle(Paint.Style.Stroke);繪制邊框,然而繪制出的是内部是圓角,外角還是直角。
2.暫隻支援文本的顯示,未設定圖檔的顯示。