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);}@Overrideprotected 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);}@Overrideprotected 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();}}}}@Overrideprotected 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);}}@Overridepublic 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() {@Overridepublic void onAnimationStart(Animator animation) {if (listener != null) {listener.onStart(); // 通知动画开始}}@Overridepublic 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; // 图标资源IDpublic int color; // 扇区颜色public Bitmap icon; // 图标Bitmappublic 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大小*/@Overrideprotected 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内容*/@Overrideprotected 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);}/*** 处理触摸事件*/@Overridepublic 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;}@Overrideprotected 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;}
}