Android自定义游戏view积累

发布于:2025-08-02 ⋅ 阅读:(11) ⋅ 点赞:(0)

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;
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到