Android抽奖转盘LotteryView

简介: 一次吃饭,突然听到有一个前端朋友有个需求是做一个抽奖的转盘,然后我就思考了下用Android原生的话要怎么实现这个转盘,于是我就自己花时间做个Demo试试。

一次吃饭,突然听到有一个前端朋友有个需求是做一个抽奖的转盘,然后我就思考了下用Android原生的话要怎么实现这个转盘,于是我就自己花时间做个Demo试试。因为时间毕竟是用下班的时间弄的,所以可以说只是个半成品,但是核心功能是能实现的。

一.开发步骤

要开发这样的一个转盘,我随便先在网上找张图看看大概要实现的效果

img_db735a58b5f1540ef5b44fee079f4e86.png

差不多是这样子的一个效果,然后我觉得主要可以分为以下几步去开发
(1)把转盘给画出来
(2)让转盘转动
(3)把转盘其他零件装上
(4)设置其它的一些属性供特殊的需求使用

二.画转盘

按照上面的步骤,我们先要把转盘给画出来。注意,我这里说的转盘指的是途中只包含圆盘的部分,也就是会转的部分,像开始抽奖的按钮和边框是不包含的,至于为什么我要这样设计,我只能告诉你为了复用。

1.最终实现的效果

先来看看我自己做的最终实现的一个效果(懒得重新截图,那个提示直接无视就行)


img_23b1e675e651a57fa33d5472e80368a8.png

做得比较简单,主要的就是在没一个扇形中加一张图和一段文字。

2.思路

思路其实很简单,就是把这个轮盘用自定义view给画出来,那么要画出来,肯定最主要的是用Paint + Canvas在onDraw中把图给画出来。所以说要先熟悉Paint 和Canvas 的基本使用方法。

好,看完了Paint 和Canvas的用法之后,你肯定知道可以画出一个扇形的效果,和Canvas可以旋转画布。我的思路是:我要做的就是在某一个起始的地方画出一个扇形,然后给扇形中添加图片和文字。然后再转动画布扇形的角度,这样就相当于回到起始的地方,这样做的好处就是不用频繁的去计算你要画的坐标。就像你写字,你可以选择移动手和移动纸,而我选择移动纸。

3.代码解析

先测量

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        widthMode = MeasureSpec.getMode(widthMeasureSpec);
        widthSize = MeasureSpec.getSize(widthMeasureSpec);
        heightMode = MeasureSpec.getMode(heightMeasureSpec);
        heightSize = MeasureSpec.getSize(heightMeasureSpec);

            if (radius != 0) {
                setMeasuredDimension(2 * radius, 2 *radius);
            } else {
                setMeasuredDimension(widthSize, heightSize);
            }
       
    }

我这里定义了一个半径radius 的属性来决定控件的大小。

测量后我们把圆盘给画出来

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (builder != null) {
            // 判断是否有扇形
            if (builder.disks == null || builder.disks.size() <= 0){
                return;
            }

            // 设置扇形画笔
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setStrokeWidth(2);
            mPaint.setAntiAlias(true);
            // 设置分割线画笔
            if (builder.division > 0) {
                divisionPaint.setStyle(Paint.Style.FILL);
                divisionPaint.setStrokeWidth(builder.division);
                divisionPaint.setColor(builder.divisionColor);
            }

            RectF f = new RectF(0, 0, 2 * builder.radius, 2 * builder.radius);
            for (int i = 0; i < builder.disks.size(); i++) {

                //设置每个扇形的文字
                textPaint.setColor(builder.disks.get(i).textColor);
                textPaint.setTextSize(builder.disks.get(i).textSize);

                // 设置每个扇形的颜色
                if (builder.disks.get(i).bgColor != -1){
                    mPaint.setColor(builder.disks.get(i).bgColor);
                }else {
                    mPaint.setColor(Color.WHITE);
                }

                // 设置绘制图片的区域
                Rect rect = null;
                if (builder.picLocations != null && builder.picLocations.length > 0){
                    rect = new Rect(builder.picLocations[0],builder.picLocations[1],builder.picLocations[2],builder.picLocations[3]);
                }else {
                    // 设为默认位置
                    // todo 计算默认方位有错,自行修改
                    rect = new Rect((int) (cX + builder.radius / 3),
                            (int)(cY + Math.sin(180/builder.disks.size() * Math.PI / 180) + 50),
                            (int) (cX + builder.radius / 3 + 150),
                            (int)(cY + Math.sin(180/builder.disks.size() * Math.PI / 180) + 200));
                }

                // 设置每个扇形的角度
                if (builder.angles != null && builder.angles.length > 0){
                    // 绘制扇形区域
                    canvas.drawArc(f, 0, builder.angles[i], true, mPaint);
                    // 绘制分割线
                    if (builder.division > 0) {
                        canvas.drawLine(cX, cY, cX + builder.radius, cY, divisionPaint);
                    }
                    // 绘制图标
                    if (builder.disks.get(i).iconBitmap != null) {
                        canvas.drawBitmap(builder.disks.get(i).iconBitmap, null, rect, null);
                    }
                    // 绘制文字
                    canvas.drawText(builder.disks.get(i).textContent,
                            (float) (cX + Math.cos(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            (float) (cY + Math.sin(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            textPaint);
                    //旋转画布
                    canvas.rotate(builder.angles[i], cX, cY);
                }else {
                    canvas.drawArc(f, 0, 360/builder.disks.size(), true, mPaint);
                    if (builder.division > 0) {
                        canvas.drawLine(cX, cY, cX + builder.radius, cY, divisionPaint);
                    }
                    if (builder.disks.get(i).iconBitmap != null) {
                        canvas.drawBitmap(builder.disks.get(i).iconBitmap, null, rect, null);
                    }
                    canvas.drawText(builder.disks.get(i).textContent,
                            (float) (cX + Math.cos(180/builder.disks.size() * Math.PI / 180) * (builder.radius-36)),
                            (float) (cY + Math.sin(180/builder.disks.size() * Math.PI / 180) * (builder.radius-36)),
                            textPaint);
                    canvas.rotate(360/builder.disks.size(), cX, cY);
                }


            }

        }

    }

因为我的代码是已经全部写出来了,时间关系我也没办法再删回去一步一步说,所以这里就整体的一起说这个流程。
(1)首先我这里因为供很多属性给用户设置,所以在自定义VIew中用了builde模式,如果不知道builde模式建议先去了解。所以第一步先判断builde是不是空

 if (builder != null) {

(2)然后第二步,我这里画的扇形是用一个扇形对象来决定的,也就是说我这个类要画出怎样的扇形的效果,是从外部传进来一个扇形数组来决定的,我定义扇形的对象叫做DiskEntity,可以先看看这个类(注释里面也写得很清楚哪个属性代表什么)

public class DiskEntity {
    // 背景颜色
    public int bgColor = -1;
    // 文字内容
    public String textContent;
    // Icon
    public Bitmap iconBitmap;
    // 字体颜色
    public int textColor = Color.BLACK;
    // 字体尺寸
    public int textSize = 36;
}

所以才要先判断传进来的扇形数组,也就是List<DiskEntity>是否至少有一个扇形,要是没有的话就没必要画了。
(3)第三步设置画笔

             // 设置扇形画笔
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setStrokeWidth(2);
            mPaint.setAntiAlias(true);
            // 设置分割线画笔
            if (builder.division > 0) {
                divisionPaint.setStyle(Paint.Style.FILL);
                divisionPaint.setStrokeWidth(builder.division);
                divisionPaint.setColor(builder.divisionColor);
            }

这个应该不难理解,上面的mPaint是画扇形的画笔,下面的divisionPaint是画分割线的画笔,我这个View里面暂时定义了三个画笔


img_d5c58fbf33330d121f1e7b42012a7bbd.png

这里还需要注意的是setAntiAlias一定要写,不然你的圆会画得有锯齿的效果。
(4)设置扇形的区域

RectF f = new RectF(0, 0, 2 * builder.radius, 2 * builder.radius);

要是你看不懂这一句的话,说明你有必要好好学学Paint 和Canvas的用法,所以这句也没什么好解释的。
(5)开始循环画扇形

for (int i = 0; i < builder.disks.size(); i++) {

可以看出我的循环次数是builder.disks.size(),也就是扇形的次数。然后中间也加了注释,没必要多解释,之后我有一个判断

if (builder.angles != null && builder.angles.length > 0){

这是什么意思呢,angles是我的一个从外面传进来的角度数组。为了就是实现扇形不等分圆的情况,所以这个判断的意思是这个圆是否是等分成多个扇形,还是每个扇形的角度都不一定一样。所以里面的代码基本都一样,我们就看其中一份就好

                     // 绘制扇形区域
                    canvas.drawArc(f, 0, builder.angles[i], true, mPaint);
                    // 绘制分割线
                    if (builder.division > 0) {
                        canvas.drawLine(cX, cY, cX + builder.radius, cY, divisionPaint);
                    }
                    // 绘制图标
                    if (builder.disks.get(i).iconBitmap != null) {
                        canvas.drawBitmap(builder.disks.get(i).iconBitmap, null, rect, null);
                    }
                    // 绘制文字
                    canvas.drawText(builder.disks.get(i).textContent,
                            (float) (cX + Math.cos(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            (float) (cY + Math.sin(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            textPaint);
                    //旋转画布
                    canvas.rotate(builder.angles[i], cX, cY);

可以看出我还是很良心的加了注释,先看扇形区域,然后判断是不是需要分割线,如果需要分割线就画出来,然后判断是否有图片,有就画出来,然后画文字。最后一步旋转画布
从这里的代码就可以看出我用旋转画布的方法来画每个扇形的好处,就是不用每个扇形都去设置它的位置,我这样就很灵活。

上面的代码也不是很多,但是这样做就能灵活的把一个圆盘给画出来,看不懂不要紧,我最后会把所有代码和项目地址给贴出来。

4.builde模式

我上面说了为了让这个圆盘更加灵活,所以我用了Builde模式,看看里面包含哪些属性

public static class DiskViewBuilder{
        private int radius = 0;
        private List<DiskEntity> disks = new ArrayList<>();
        private int[] angles;
        private int[] probabilities;
        private int[] picLocations;
        private int division = 0;
        private int divisionColor = -1;

        /**
         * 设置半径
         */
        public DiskViewBuilder setRadius(int radius){
            this.radius = radius;
            return this;
        }
        /**
         *  设置扇形对象数组
         */
        public DiskViewBuilder setDiskList(List<DiskEntity> disks){
            this.disks = disks;
            return this;
        }
        /**
         * 设置角度的数组
         */
        public DiskViewBuilder setAngles(int[] angles){
            this.angles = angles;
            return this;
        }

        /**
         * 设置概率数组
         */
        public DiskViewBuilder setProbabilities(int[] probabilities){
            this.probabilities = probabilities;
            return this;
        }

        /**
         * 设置图片的距离
         */
        public DiskViewBuilder setPicLocations(int[] picLocations){
            this.picLocations = picLocations;
            return this;
        }

        /**
         * 设置分割线
         */
        public DiskViewBuilder setDivision(int division,int divisionColor){
            this.division = division;
            this.divisionColor = divisionColor;
            return this;
        }

        public DiskView builder(Context context){
            DiskView diskView = new DiskView(context);
            diskView.setBuilder(this);
            return diskView;
        }

    }

我干脆把全部代码贴出来算了,免得贴一部分有人看不懂,能看懂的可以直接跳过下面的源码

  public class DiskView extends View{

    private int widthMode;
    private int widthSize;
    private int heightMode;
    private int heightSize;
    private float cX;
    private float cY;
    private DiskViewBuilder builder;

    private Paint mPaint;//饼状画笔
    private Paint textPaint;// 文字画笔
    private Paint divisionPaint; //分割线画笔

    public DiskView(Context context) {
        super(context);
        init();
    }

    public DiskView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DiskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setBuilder(DiskViewBuilder builder){
        this.builder = builder;
        cX = builder.radius;
        cY = builder.radius;
    }

    private void init(){
        mPaint = new Paint();
        textPaint = new Paint();
        divisionPaint = new Paint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        widthMode = MeasureSpec.getMode(widthMeasureSpec);
        widthSize = MeasureSpec.getSize(widthMeasureSpec);
        heightMode = MeasureSpec.getMode(heightMeasureSpec);
        heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (builder != null) {
            if (builder.radius != 0) {
                setMeasuredDimension(2 * builder.radius, 2 * builder.radius);
            } else {
                setMeasuredDimension(widthSize, heightSize);
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (builder != null) {
            // 判断是否有扇形
            if (builder.disks == null || builder.disks.size() <= 0){
                return;
            }

            // 设置扇形画笔
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setStrokeWidth(2);
            mPaint.setAntiAlias(true);
            // 设置分割线画笔
            if (builder.division > 0) {
                divisionPaint.setStyle(Paint.Style.FILL);
                divisionPaint.setStrokeWidth(builder.division);
                divisionPaint.setColor(builder.divisionColor);
            }

            RectF f = new RectF(0, 0, 2 * builder.radius, 2 * builder.radius);
            for (int i = 0; i < builder.disks.size(); i++) {

                //设置每个扇形的文字
                textPaint.setColor(builder.disks.get(i).textColor);
                textPaint.setTextSize(builder.disks.get(i).textSize);

                // 设置每个扇形的颜色
                if (builder.disks.get(i).bgColor != -1){
                    mPaint.setColor(builder.disks.get(i).bgColor);
                }else {
                    mPaint.setColor(Color.WHITE);
                }

                // 设置绘制图片的区域
                Rect rect = null;
                if (builder.picLocations != null && builder.picLocations.length > 0){
                    rect = new Rect(builder.picLocations[0],builder.picLocations[1],builder.picLocations[2],builder.picLocations[3]);
                }else {
                    // 设为默认位置
                    // todo 计算默认方位有错,自行修改
                    rect = new Rect((int) (cX + builder.radius / 3),
                            (int)(cY + Math.sin(180/builder.disks.size() * Math.PI / 180) + 50),
                            (int) (cX + builder.radius / 3 + 150),
                            (int)(cY + Math.sin(180/builder.disks.size() * Math.PI / 180) + 200));
                }

                // 设置每个扇形的角度
                if (builder.angles != null && builder.angles.length > 0){
                    // 绘制扇形区域
                    canvas.drawArc(f, 0, builder.angles[i], true, mPaint);
                    // 绘制分割线
                    if (builder.division > 0) {
                        canvas.drawLine(cX, cY, cX + builder.radius, cY, divisionPaint);
                    }
                    // 绘制图标
                    if (builder.disks.get(i).iconBitmap != null) {
                        canvas.drawBitmap(builder.disks.get(i).iconBitmap, null, rect, null);
                    }
                    // 绘制文字
                    canvas.drawText(builder.disks.get(i).textContent,
                            (float) (cX + Math.cos(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            (float) (cY + Math.sin(builder.angles[i]/2 * Math.PI / 180) * (builder.radius-36)),
                            textPaint);
                    //旋转画布
                    canvas.rotate(builder.angles[i], cX, cY);
                }else {
                    canvas.drawArc(f, 0, 360/builder.disks.size(), true, mPaint);
                    if (builder.division > 0) {
                        canvas.drawLine(cX, cY, cX + builder.radius, cY, divisionPaint);
                    }
                    if (builder.disks.get(i).iconBitmap != null) {
                        canvas.drawBitmap(builder.disks.get(i).iconBitmap, null, rect, null);
                    }
                    canvas.drawText(builder.disks.get(i).textContent,
                            (float) (cX + Math.cos(180/builder.disks.size() * Math.PI / 180) * (builder.radius-36)),
                            (float) (cY + Math.sin(180/builder.disks.size() * Math.PI / 180) * (builder.radius-36)),
                            textPaint);
                    canvas.rotate(360/builder.disks.size(), cX, cY);
                }


            }

        }

    }

    public static class DiskViewBuilder{
        private int radius = 0;
        private List<DiskEntity> disks = new ArrayList<>();
        private int[] angles;
        private int[] probabilities;
        private int[] picLocations;
        private int division = 0;
        private int divisionColor = -1;

        /**
         * 设置半径
         */
        public DiskViewBuilder setRadius(int radius){
            this.radius = radius;
            return this;
        }
        /**
         *  设置扇形对象数组
         */
        public DiskViewBuilder setDiskList(List<DiskEntity> disks){
            this.disks = disks;
            return this;
        }
        /**
         * 设置角度的数组
         */
        public DiskViewBuilder setAngles(int[] angles){
            this.angles = angles;
            return this;
        }

        /**
         * 设置概率数组
         */
        public DiskViewBuilder setProbabilities(int[] probabilities){
            this.probabilities = probabilities;
            return this;
        }

        /**
         * 设置图片的距离
         */
        public DiskViewBuilder setPicLocations(int[] picLocations){
            this.picLocations = picLocations;
            return this;
        }

        /**
         * 设置分割线
         */
        public DiskViewBuilder setDivision(int division,int divisionColor){
            this.division = division;
            this.divisionColor = divisionColor;
            return this;
        }

        public DiskView builder(Context context){
            DiskView diskView = new DiskView(context);
            diskView.setBuilder(this);
            return diskView;
        }

    }



}

可以看出其实也没有多复杂,这个view就搭建完了。

5.创建圆盘控件

看了源码后我们来看看怎么去创建这个View,这里我命名DiskView而不是LotteryView是因为它只是一部分,而且其它功能也可以使用这个View,先不多说,先看看效果。
要实现上面图中的那种效果,可以在使用的地方调用

diskView = new DiskView.DiskViewBuilder()
                .setRadius(400)
                .setDiskList(disks)
                .builder(this);
linearLayout.addView(diskView);

然后因为是builder模式嘛,我们还可以快速的实现其它效果
比如加个角度数组进去

diskView = new DiskView.DiskViewBuilder()
                .setRadius(400)
                .setDiskList(disks)
                .setAngles(new int[]{30,30,120,60,30,90})
                .builder(this);

img_71e905b1cc41e1fe3dc41ce33a491b51.png

可以看到实现角度不同的扇形。但是图片是乱的, 因为图片的位置算法我没写好,我自己是写死方位的,没办法,时间问题,但等下我会把图片要怎么显示才好的思路说一下。
再比如说我们加个分割线

diskView = new DiskView.DiskViewBuilder()
                .setRadius(400)
                .setDiskList(disks)
                .setDivision(5,Color.WHITE)
                .builder(this);
img_937e4d62a18b2c5f50815185ee4ca993.png

我加了白色的分割线,认真看还是可以看出的,请注意看,这个效果像什么,没错,加上上面的角度不同,这个两个效果是不是像饼状的统计图,这也就是我一开始这样设计这个类的原因,并不是只有转盘可以使用这个类。

三.旋转

从上面的操作中我们可以把这个图给画出来了,那我们要怎么让它旋转呢,很巧的是Android里面正好有一个东西能很好的实现这个效果,也很符合这个思想,叫属性动画。
做法也很简单,我就直接贴代码了

/**
 *  动画帮助类
 */
public class AdminHelper {

    /**
     * 转盘动画
     * 必定转3圈,然后再随机转0—4圈,第二个随机数表示最后停止的位置
     */
    public static void showLottery(View view){
        int n = RandomManager.getRandomI(4);
        int k = RandomManager.getRandomI((int) (360f));

        ObjectAnimator anim = ObjectAnimator.ofFloat(view, "rotation", 0f,k+(3+n)*360f);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.setDuration(5000);
        anim.start();
    }

}
/**
 *  管理随机数
 */
public class RandomManager {

    public static float getRandomF(){
        Random random = new Random();
        return random.nextFloat();
    }

    public static int getRandomI(){
        Random random = new Random();
        return random.nextInt();
    }

    public static int getRandomI(int k){
        Random random = new Random();
        return random.nextInt(k);
    }

    /**
     * 设置概率返回所属范围
     * @param probabilities 概率数组
     * @return 返回属于哪个概率的下标
     */
    public static int getIndexFormProbability(float[] probabilities){
        //判断数组所有数累加书否等于1,如果总计不为100%,则返回-1
        float count = 0;
        for (int i = 0; i < probabilities.length; i++) {
            count += probabilities[i];
        }
        if (count != (float) 1){
            return -1;
        }

        //获取随机数
        Random random = new Random();
        float result = random.nextFloat();

        // 把 1 分成很多段,然后判断最后的数会落在哪一段
        count = 0;
        for (int i = 0; i < probabilities.length; i++) {
            count += probabilities[i];
            if (result < count){
               return i;
            }
        }

        return -1;
    }

}

然后我们现在假设点击一个地方让它转动

TextView textView = findViewById(R.id.tv);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AdminHelper.showLottery(diskView);
            }
        });

先看看效果我再解释代码


img_4acd2a6ebf9c569a21389ed5fe770f1d.gif

虽然gif有点卡,但是机子上面是没问题的。然后我现在见到讲解一下我的代码
首先动画的时候我先获取两个随机数

        int n = RandomManager.getRandomI(4);
        int k = RandomManager.getRandomI((int) (360f));

这是什么意思呢,我第一个随机数是设置圈数,也就是我要至少转3圈,转完3圈之后再随机转0-4圈,这也就是下面动画时写的3+n。第二个随机数是决定转到哪个选项,所以准确来说第一个转多少圈只是为了视觉效果,第二个才是决定最终会停在哪里。其他的都还挺简单的,然后注意下我设了这个属性

anim.setInterpolator(new AccelerateDecelerateInterpolator());

这个主要是模拟一个刚开始转会慢,然后速度变快,到最后快停下的时候再变慢,可能我gif演示得不够明显,自己可以试试,但是这个体验效果一定要,这也是Android5.0之后提出的思想。

最后说说随机数里面的一个方法getIndexFormProbability
这个方法是什么意思呢,按照我上面的做法,其实转到哪个地方的概率是相同的,你想想,抽奖的时候一般能让你概率相同吗?要是有一个iphoneX,你能让他和“谢谢惠顾”的概率是五五开吗,所以我这里就做了一个设置概率的办法,一般来说中iphoneX的概率大概有0.005以下,当然这是我猜的。

四.拼接轮盘与不足

首先说说拼接轮盘,我这里没拼接,因为时间不足,其实做法很简单,就是弄个viewgroup之类的,然后装背景,装我写的轮盘,装指针,很简单。我没写是因为我确实下班的时间不是很多,而且我也没指针这些素材。
然后就是不同概率的方法我写出来了,但是没有写这个切换的方法。
还有就是转盘的文字和图片的方向有点怪,那是因为我设置的原始角度是右下角,想要正常的话设成正下方就行,我就懒得重新再调了,毕竟计算那些坐标值有点烦。
还有一点就是我这里只设置了第一个构造方法,如果真要做出一个自定义view还是要设置一些属性供第二个构造方法比较好,这个也比较简单我就不重复写了。

可以看出我这里只是做出了个半成品,把核心的功能实现了,但是并没有完整的把这个控件搭出来,那是因为一般来说的抽奖页面都是用H5来做的,因为做起来也比原生的方便,而且功能会更灵活,基本都是不会使用原生来做,你想想,如果iphoneX抽完了,我H5可以换一个商品或者打把叉之类的,但是你原生做不到啊,还要更新,所以前端来做这个功能会更灵活,我这里主要是为了证明我们原生的也可以使用自己的方法实现这个功能,而且也不会很繁杂

五.项目地址

考虑到可能我表达会有问题,解释不清楚,所以干脆还是把源码给发出来好了
https://github.com/994866755/handsomeYe.LotteryDemo

目录
相关文章
|
Android开发
Android CircleMenu:旋转转盘选择Menu
 Android CircleMenu:旋转转盘选择Menu Android CircleMenu是一个可以自由旋转类似转盘样式的选择Menu。
1084 0
|
XML 前端开发 Android开发
基于 SurfaceView 详解 android 幸运大转盘,附带实例app
<h1><span style="color:#3366FF">基于 SurfaceView 详解 android 幸运大转盘,附带实例app</span></h1> <h3>     <span style="color:#3366FF"> 首先说一下,幸运大转盘,以及SurfaceView是在看了也为大神的博客,才有了比较深刻的理解,当然这里附上这位大神的博客地址:<a target
2715 0
|
22天前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
22天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
83 1
|
25天前
|
Android开发
Android开发表情emoji功能开发
本文介绍了一种在Android应用中实现emoji表情功能的方法,通过将图片与表情字符对应,实现在`TextView`中的正常显示。示例代码展示了如何使用自定义适配器加载emoji表情,并在编辑框中输入或删除表情。项目包含完整的源码结构,可作为开发参考。视频演示和源码详情见文章内链接。
52 4
Android开发表情emoji功能开发
|
23天前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
53 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
5天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
22 5
|
4天前
|
设计模式 IDE Java
探索安卓开发:从新手到专家的旅程
【10月更文挑战第22天】 在数字时代的浪潮中,移动应用开发如同一座金矿,吸引着无数探险者。本文将作为你的指南针,指引你进入安卓开发的广阔天地。我们将一起揭开安卓平台的神秘面纱,从搭建开发环境到掌握核心概念,再到深入理解安卓架构。无论你是初涉编程的新手,还是渴望进阶的开发者,这段旅程都将为你带来宝贵的知识和经验的财富。让我们开始吧!