Android自定义游戏view积累
1.自定义转盘View(LotteryView)
/**
* 自定义抽奖转盘View
* 功能:实现可旋转的抽奖转盘,支持自定义奖品数量和样式
*/
public class LotteryView extends View {
// 默认奖品数量
private static final int DEFAULT_COUNT = 6;
// 中心指示器默认缩放比例
private static final float DEFAULT_CENTER_SCALE = 0.2f;
// 绘制工具
private Paint arcPaint; // 绘制扇形区域的画笔
private Paint textPaint; // 绘制文本的画笔
private Paint iconPaint; // 绘制图标的画笔
private RectF arcRectF; // 转盘绘制区域
// 几何参数
private float centerX; // 转盘中心X坐标
private float centerY; // 转盘中心Y坐标
private float radius; // 转盘半径
// 数据相关
private List<Prize> prizes = new ArrayList<>(); // 奖品列表
private int prizeCount = DEFAULT_COUNT; // 当前奖品数量
// 旋转控制
private float startAngle = 0; // 起始角度
private float currentAngle = 0; // 当前旋转角度
private ValueAnimator rotateAnimator; // 旋转动画控制器
// 其他组件
private Bitmap indicatorBitmap; // 中心指示器图片
private float centerScale = DEFAULT_CENTER_SCALE; // 指示器缩放比例
private OnLotteryListener listener; // 事件监听器
// 构造方法(系统自动调用)
public LotteryView(Context context) {
this(context, null);
}
public LotteryView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LotteryView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs); // 初始化View
}
/**
* 初始化View
*/
private void init(Context context, AttributeSet attrs) {
// 1. 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LotteryView);
centerScale = ta.getFloat(R.styleable.LotteryView_centerScale, DEFAULT_CENTER_SCALE);
ta.recycle(); // 必须回收TypedArray
// 2. 初始化绘制工具
arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
arcPaint.setStyle(Paint.Style.FILL);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(sp2px(14));
textPaint.setTextAlign(Paint.Align.CENTER);
iconPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
iconPaint.setFilterBitmap(true); // 开启抗锯齿
iconPaint.setDither(true); // 开启防抖动
// 3. 初始化绘制区域(具体尺寸在onSizeChanged中确定)
arcRectF = new RectF();
// 4. 加载中心指示器图片
indicatorBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_indicator);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 强制View为正方形(取宽高的最小值)
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 1. 计算中心点和半径(保留10%边距)
centerX = w / 2f;
centerY = h / 2f;
radius = Math.min(w, h) / 2f * 0.9f;
// 2. 设置转盘绘制区域
arcRectF.set(centerX - radius, centerY - radius,
centerX + radius, centerY + radius);
// 3. 加载奖品图标(此时已知View尺寸,可以正确缩放图标)
loadPrizeIcons();
}
/**
* 加载并缩放奖品图标
*/
private void loadPrizeIcons() {
for (Prize prize : prizes) {
if (prize.icon == null && prize.iconResId != 0) {
// 1. 加载原始图片
Bitmap originalBitmap = BitmapFactory.decodeResource(
getResources(), prize.iconResId);
// 2. 计算缩放尺寸(图标大小为半径的15%)
int iconSize = (int) (radius * 0.15f);
// 3. 创建缩放后的图片
prize.icon = Bitmap.createScaledBitmap(
originalBitmap, iconSize, iconSize, true);
// 4. 回收原始图片(避免内存泄漏)
if (!originalBitmap.isRecycled()) {
originalBitmap.recycle();
}
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1. 绘制转盘
drawLottery(canvas);
// 2. 绘制中心指示器
drawIndicator(canvas);
}
/**
* 绘制抽奖转盘
*/
private void drawLottery(Canvas canvas) {
canvas.save();
// 应用当前旋转角度
canvas.rotate(currentAngle, centerX, centerY);
// 计算每个扇区的角度
float angle = 360f / prizeCount;
// 绘制每个奖品扇区
for (int i = 0; i < prizeCount; i++) {
// 1. 设置扇区颜色并绘制
arcPaint.setColor(prizes.get(i).color);
canvas.drawArc(arcRectF, startAngle + i * angle, angle, true, arcPaint);
// 2. 绘制奖品文本和图标
drawTextAndIcon(canvas, i, angle);
}
canvas.restore();
}
/**
* 绘制奖品文本和图标
*/
private void drawTextAndIcon(Canvas canvas, int position, float angle) {
// 计算文本中心角度(扇区中间位置)
float textAngle = startAngle + position * angle + angle / 2;
float textRadius = radius * 0.7f; // 文本绘制半径(70%半径处)
// 1. 绘制奖品名称(保持文字正向)
canvas.save();
// 旋转画布使文字沿切线方向
canvas.rotate(textAngle - 90, centerX, centerY);
String text = prizes.get(position).name;
// 在80%半径处绘制文本
canvas.drawText(text, centerX, centerY - radius * 0.8f, textPaint);
canvas.restore();
// 2. 绘制奖品图标(如果有)
if (prizes.get(position).icon != null) {
float iconRadius = radius * 0.5f; // 图标绘制半径(50%半径处)
// 计算图标中心坐标
float iconX = (float) (centerX + iconRadius * Math.cos(Math.toRadians(textAngle)));
float iconY = (float) (centerY + iconRadius * Math.sin(Math.toRadians(textAngle)));
// 创建图标绘制区域
Rect rect = new Rect(
(int) (iconX - prizes.get(position).icon.getWidth() / 2),
(int) (iconY - prizes.get(position).icon.getHeight() / 2),
(int) (iconX + prizes.get(position).icon.getWidth() / 2),
(int) (iconY + prizes.get(position).icon.getHeight() / 2)
);
canvas.drawBitmap(prizes.get(position).icon, null, rect, iconPaint);
}
}
/**
* 绘制中心指示器
*/
private void drawIndicator(Canvas canvas) {
if (indicatorBitmap != null) {
// 计算指示器大小和位置
int indicatorSize = (int) (radius * centerScale);
Rect rect = new Rect(
(int) (centerX - indicatorSize / 2),
(int) (centerY - indicatorSize / 2),
(int) (centerX + indicatorSize / 2),
(int) (centerY + indicatorSize / 2)
);
canvas.drawBitmap(indicatorBitmap, null, rect, iconPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 旋转时忽略触摸事件
if (isRotating()) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 计算触摸点与中心的距离
float dx = event.getX() - centerX;
float dy = event.getY() - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy);
// 如果点击了中心区域(半径*缩放比例范围内)
if (distance < radius * centerScale) {
if (listener != null) {
listener.onCenterClick(); // 触发点击回调
}
}
break;
}
return super.onTouchEvent(event);
}
/**
* 设置奖品数据
*/
public void setPrizes(List<Prize> prizes) {
this.prizes = prizes;
this.prizeCount = prizes.size();
invalidate(); // 触发重绘
}
/**
* 开始旋转动画
* @param targetPosition 目标奖品位置(0-based)
* @param listener 动画监听器
*/
public void startRotateAnimation(int targetPosition, OnLotteryListener listener) {
this.listener = listener;
// 计算目标角度(3-5圈 + 补偿当前角度 + 定位到目标扇区)
float targetAngle = 360 * (3 + (int)(Math.random() * 3)) + // 基础3-5圈
(360 - currentAngle) + // 补偿当前角度
(360f / prizeCount) * (prizeCount - targetPosition - 0.5f); // 定位到目标
// 停止正在进行的动画
if (rotateAnimator != null && rotateAnimator.isRunning()) {
rotateAnimator.cancel();
}
// 创建旋转动画
rotateAnimator = ValueAnimator.ofFloat(currentAngle, targetAngle);
rotateAnimator.setDuration(5000); // 5秒动画
rotateAnimator.setInterpolator(new DecelerateInterpolator()); // 减速效果
// 动画更新监听
rotateAnimator.addUpdateListener(animation -> {
currentAngle = (float) animation.getAnimatedValue();
invalidate(); // 更新UI
});
// 动画生命周期监听
rotateAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (listener != null) {
listener.onStart(); // 通知动画开始
}
}
@Override
public void onAnimationEnd(Animator animation) {
// 计算最终中奖位置
float normalizedAngle = currentAngle % 360;
if (normalizedAngle < 0) normalizedAngle += 360;
int position = (int) ((360 - normalizedAngle) % 360 / (360f / prizeCount));
position = Math.max(0, Math.min(position, prizeCount - 1)); // 边界保护
if (listener != null) {
listener.onEnd(position, prizes.get(position)); // 通知结果
}
}
});
rotateAnimator.start();
}
/**
* 判断是否正在旋转
*/
public boolean isRotating() {
return rotateAnimator != null && rotateAnimator.isRunning();
}
/**
* sp转px工具方法
*/
private int sp2px(float spValue) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
spValue,
getResources().getDisplayMetrics()
);
}
/**
* 抽奖事件监听接口
*/
public interface OnLotteryListener {
void onStart(); // 旋转开始
void onEnd(int position, Prize prize); // 旋转结束
default void onCenterClick() {} // 中心点击(可选实现)
}
/**
* 奖品数据类
*/
public static class Prize {
public String name; // 奖品名称
public int iconResId; // 图标资源ID
public int color; // 扇区颜色
public Bitmap icon; // 图标Bitmap
public Prize(String name, int iconResId, int color) {
this.name = name;
this.iconResId = iconResId;
this.color = color;
}
}
}
2.刮刮卡自定义View(ScratchCardView)
/**
* 刮刮卡自定义View
* 功能:实现刮刮卡效果,支持奖品设置、中奖概率控制、刮开面积检测等
*/
public class ScratchCardView extends View {
// 绘制相关对象
private Paint mPaint; // 刮擦效果画笔
private Path mPath; // 记录用户刮擦路径
private Bitmap mBitmap; // 刮擦层位图
private Canvas mCanvas; // 刮擦层画布
// 奖品数据
private List<GamePrize> mPrizes = new ArrayList<>();
private GamePrize mCurrentPrize; // 当前奖品
private boolean mIsWinner; // 是否中奖
private float mWinProbability = 0.1f; // 中奖概率(默认10%)
// 回调接口
public interface OnScratchListener {
void onScratchStart(); // 开始刮卡
void onScratchProgress(float progress); // 刮卡进度
void onScratchComplete(boolean isWinner, GamePrize prize); // 刮卡完成
}
private OnScratchListener mListener;
// 构造方法
public ScratchCardView(Context context) {
super(context);
init();
}
public ScratchCardView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScratchCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化方法
*/
private void init() {
// 初始化画笔
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(50);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mPath = new Path();
}
/**
* 测量View大小
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
// 重新创建位图(如果尺寸变化)
if (mBitmap == null || width != mBitmap.getWidth() || height != mBitmap.getHeight()) {
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
}
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mCanvas.drawColor(Color.GRAY); // 刮擦层默认灰色
}
}
/**
* 绘制View内容
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制奖品背景
if (mCurrentPrize != null) {
canvas.drawColor(mCurrentPrize.getColor());
}
// 绘制刮擦层
if (mBitmap != null) {
canvas.drawBitmap(mBitmap, 0, 0, null);
}
// 绘制当前路径
canvas.drawPath(mPath, mPaint);
}
/**
* 处理触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mListener != null) mListener.onScratchStart();
mPath.reset();
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(x, y);
if (mCanvas != null) {
mCanvas.drawPath(mPath, mPaint);
}
// 计算并回调刮擦进度
if (mListener != null) {
mListener.onScratchProgress(getScratchProgress());
}
break;
case MotionEvent.ACTION_UP:
// 检测是否刮开足够面积
if (isScratchedEnough() && mListener != null) {
mListener.onScratchComplete(mIsWinner, mCurrentPrize);
}
break;
}
invalidate();
return true;
}
/**
* 获取当前刮开进度(0-1)
*/
private float getScratchProgress() {
if (mBitmap == null) return 0;
// 采样检测(优化性能)
int sampleStep = 10;
int transparentCount = 0;
int totalSamples = 0;
for (int x = 0; x < mBitmap.getWidth(); x += sampleStep) {
for (int y = 0; y < mBitmap.getHeight(); y += sampleStep) {
if (mBitmap.getPixel(x, y) == Color.TRANSPARENT) {
transparentCount++;
}
totalSamples++;
}
}
return (float)transparentCount / totalSamples;
}
/**
* 检查是否刮开足够面积
*/
private boolean isScratchedEnough() {
return getScratchProgress() > 0.5f; // 超过50%视为刮开
}
/**
* 随机选择奖品
*/
public void randomSelectPrize() {
if (mPrizes.isEmpty()) return;
Random random = new Random();
if (random.nextFloat() < mWinProbability) {
// 中奖:从奖品中随机选择(排除最后一个"谢谢参与")
int winnerIndex = random.nextInt(mPrizes.size() - 1);
mCurrentPrize = mPrizes.get(winnerIndex);
mIsWinner = true;
} else {
// 未中奖:选择最后一个"谢谢参与"
mCurrentPrize = mPrizes.get(mPrizes.size() - 1);
mIsWinner = false;
}
}
/**
* 设置奖品数据
*/
public void setPrizes(List<GamePrize> prizes) {
this.mPrizes = prizes;
if (!prizes.isEmpty()) {
mCurrentPrize = prizes.get(prizes.size() - 1); // 默认显示"谢谢参与"
}
}
/**
* 设置中奖概率
* @param probability 0-1之间的概率值
*/
public void setWinProbability(float probability) {
this.mWinProbability = Math.max(0, Math.min(1, probability));
}
/**
* 设置刮卡监听器
*/
public void setOnScratchListener(OnScratchListener listener) {
this.mListener = listener;
}
/**
* 释放资源
*/
public void release() {
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
mBitmap = null;
}
mCanvas = null;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
release();
}
}
奖品实体类GamePrize
public class GamePrize {
public String name; // 奖品名称
public int iconResId; // 奖品图标
public int color; // 背景颜色
public Bitmap icon; // 奖品名称
public GamePrize(String name, int iconResId, int color, Bitmap icon) {
this.name = name;
this.iconResId = iconResId;
this.color = color;
this.icon = icon;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIconResId() {
return iconResId;
}
public void setIconResId(int iconResId) {
this.iconResId = iconResId;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public Bitmap getIcon() {
return icon;
}
public void setIcon(Bitmap icon) {
this.icon = icon;
}
}