最近在项目中要实现一个九宫格抽奖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);
}
/**
* 将px值转换为sp值,保证文字大小不变
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale +0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*/
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);
}
/**
* 将sp值转换为px值,保证文字大小不变
*/
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.暂只支持文本的显示,未设置图片的显示。
最后git 代码连接: https://github.com/Warkey1991/RaffleView