SurfaceView实战打造农药钻石夺宝

本文通过王者荣耀抽奖实例介绍SurfaceView的应用技巧,包括SurfaceView的模板化使用、控件属性分析、绘制流程及交互逻辑。
部署运行你感兴趣的模型镜像

##1.概述
    SurfaceView是Android系统中的View的一种。然而,它又区别与普通的View。一般的View只能在UI线程中绘制,而SurfaceView却能在非UI线程中绘制,这样的结果是即使SurfaceView频繁的刷新重绘也不会阻塞主线程导致卡顿甚至ANR。
    SurfaceView在日常的开发使用中也很常见,例如VideoView和Android的游戏。这些场景均需要高频率的重绘以达到流畅的用户体验。
    本文重点不在于解析SurfaceView的原理,而是SurfaceView的实战。通过打造王者荣耀坑钱夺宝抽奖来讲解SurfaceView绘制的技巧。其中涉及一些简单的算法、动画还有几何布局。希望对读者有所帮助。控件源码地址在文章最后。
##2.SurfaceView的模板化使用
    正所谓无规矩不成方圆,有了规矩就不用自己抓破头皮想规矩了。哈哈。SurfaceView也一样。对于一般SurfaceView的绘制也有一套规矩。

public class LuckyBoard extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private Canvas mCanvas;

    private boolean isDrawing;
    private Thread drawThread;
	public LuckyBoard(Context context) {
        this(context, null);
    }

    public LuckyBoard(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LuckyBoard(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        parseAttrs(context, attrs, defStyleAttr);
        init();
    }

	 private void init() {
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        isDrawing = true;
        drawThread = new Thread(this);
        drawThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        isDrawing = false;
        drawThread.join();
        mHolder.removeCallback(this);
        mHolder = null;
        mCanvas = null;
    }

	@Override
    public void run() {
        while (isDrawing) {
	      try{
            mCanvas = mHolder.lockCanvas();
            draw();
	      }catch(Exception e){
			e.printStackTrace();
		  }finally{
			mHolder.unlockCanvasAndPost(mCanvas);
		  }
        }
    }

	void draw(){
	//绘制的内容
	}
}

    代码很简单,在surfaceCreated方法里面创建绘制线程,isDrawing设置为true,此时当isDrawing为true时将不断刷新绘制。在surfaceDestroyed方法中将isDrawing设为false,让绘制线程终止,SurfaceView将不再刷新重绘。
##3.效果图
    一般讲到这里,很多读者就会很不耐烦。心想:说那么多干嘛,快上效果图看看让我有没有读下去的动力。好的马上奉上。
12规模抽奖 8规模抽奖
    程序员容易得颈椎病。我不会告诉你其实我是想治好你们的颈椎病才这样的哈哈。知道怎么旋转图片的同学可以评论告诉我一下。gif是10fps,实际效果更流畅。
    上图分别是12奖品规模和8奖品规模的效果图。奖品规模是根据用户输入的奖品参数自适应的。后面将有讲解。
##4.使用方法
###①添加控件到layout布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.luozm.luckyboarddemo.MainActivity">

   <com.luozm.luckyboard.LuckyBoard
       android:id="@+id/luckyboard"
       android:layout_gravity="center"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

</LinearLayout>

###②Java代码初始化

public class MainActivity extends AppCompatActivity {

    LuckyBoard luckyBoard;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        luckyBoard = (LuckyBoard) findViewById(R.id.luckyboard);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
        Bitmap ake = BitmapFactory.decodeResource(getResources(), R.mipmap.ake);
        Bitmap bailishouyue = BitmapFactory.decodeResource(getResources(), R.mipmap.bailishouyue);
        Bitmap caocao = BitmapFactory.decodeResource(getResources(), R.mipmap.caocao);
        Bitmap huangzhong = BitmapFactory.decodeResource(getResources(), R.mipmap.huangzhong);
        Bitmap liubei = BitmapFactory.decodeResource(getResources(), R.mipmap.liubei);
        Bitmap xiahoudun = BitmapFactory.decodeResource(getResources(), R.mipmap.xiahoudun);
        Bitmap zhangfei = BitmapFactory.decodeResource(getResources(), R.mipmap.zhangfei);
        Bitmap zhaoyun = BitmapFactory.decodeResource(getResources(), R.mipmap.zhaoyun);
        List<LuckyAward> awards = new ArrayList<>();
        awards.add(new LuckyAward("阿珂", ake, 0.1f));
        awards.add(new LuckyAward("百里守约", bailishouyue, 0.1f));
        awards.add(new LuckyAward("曹操", caocao, 0.1f));
        awards.add(new LuckyAward("黄忠", huangzhong, 0.1f));
        awards.add(new LuckyAward("刘备", liubei, 0.1f));
        awards.add(new LuckyAward("夏侯惇", xiahoudun, 0.1f));
        awards.add(new LuckyAward("张飞", zhangfei, 0.1f));
        awards.add(new LuckyAward("赵云", zhaoyun, 0.1f));
        luckyBoard.setAvAward(new LuckyAward("谢谢惠顾", bitmap, 0f));
        luckyBoard.setAwards(awards);
        luckyBoard.setResultCallback(new LuckyBoard.ResultCallback() {
            @Override
            public void result(LuckyAward award) {
                Toast.makeText(MainActivity.this, award.getName(), Toast.LENGTH_SHORT).show();
            }
        });
    }


}

    这其中关注的方法有LuckyBoard的setAvAward、setAwrads、setResultCallback方法,它们分别的功能是设置安慰奖(不要想歪)、设置实际奖品和设置抽奖结果回调。
    以上就是使用LuckyBoard的基本操作。接下来将讲解怎么打造这个LuckyBoard。
##5.打造LuckyBoard
###①分析控件的属性
    抽象化是自定义View的第一步,分析提取控件中哪些是必须的。抽象化一般从效果图中着手。当然刚打造这控件时是没效果图的,那么只能从脑子里想象如果控件做好之后是什么样子的,从而分析提取控件的属性。如今有效果图了,那么就对照效果图分析提取控件的属性吧。
    从效果图可以看到,LuckyBoard是由一个大的矩形有序分布着小的矩形,中间有一个圆形的按钮,大致是这样的几何分布:
这里写图片描述
    图以8奖品规模为例。从上图可以抽象出一个blockSize的属性,这个blockSize可是很重要的。它指的是小矩形的宽度,小矩形的宽高比例是4:3。这样小矩形的尺寸就有了。那么大矩形也是根据小矩形的数量规模决定尺寸的。那么我们就可以重写onMeasure来定义SurfaceView的尺寸了。
     LuckyBoard抽奖轮盘中奖品是必须的,这样就抽象出一个awards的属性。它代表奖品池中的奖品列表,是一个实体类的集合。这个实体类叫LuckyAward,封装着奖品的名称、图片和获奖概率。

public class LuckyAward implements Cloneable{
    private String name;
    private Bitmap bitmap;
    private float rate;

    public LuckyAward(String name, Bitmap bitmap, float rate) {
        this.name = name;
        this.bitmap = bitmap;
        this.rate = rate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    public float getRate() {
        return rate;
    }

    public void setRate(float rate) {
        this.rate = rate;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

    最后,我们从效果图可以看到,初始时各个小矩形并没有蒙上一层阴影。点击中间的按钮后,出现了阴影并且在小矩形之间循环走动。再点下按钮,小矩形开始减速并最终停留在一个奖品中。在这里我们可以分析出控件有个mState的属性,它指的是控件当前状态,以此来决定控件行为。控件的状态有3种,分别是空闲状态(STATE_IDEL)、循环滚动状态(STATE_RUNNING)和结果产生状态(STATE_RESULT)。有了这三种状态,我们就可以在draw()这个方法中决定画什么。当然,阴影在哪个位置,我们提取一个currentPosition的属性。
###②onMeasure
    重写onMeasure方法是自定义View的基本操作,LuckyBoard控件的大小是根据blockSize和奖品规模决定的。代码如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //奖品规模为0时,设置宽高为0
        if (awards == null || awards.size() == 0) {
            setMeasuredDimension(0, 0);
        } else {
            int awardsSize = awards.size();
            int width;
            int height;
            if (awardsSize <= 8) {//奖品规模为8
                width = 2 * horizontalPadding + blockSize * 3;
                height = (int) (2 * verticalPadding + blockSize * 3 * 0.75);
            } else if (awardsSize <= 12) {//奖品规模为12
                width = 2 * horizontalPadding + blockSize * 4;
                height = (int) (2 * verticalPadding + blockSize * 4 * 0.75);
            } else if (awardsSize <= 20) {//奖品规模为20
                width = 2 * horizontalPadding + blockSize * 5;
                height = (int) (2 * verticalPadding + blockSize * 5 * 0.75);
            } else {
                throw new IllegalStateException("Awards Size must not be above of 20");
            }
            setMeasuredDimension(width, height);
        }
    }

    这样控件的大小就设置了。
###③setAvAward方法和setAwards方法
    在使用方法那部分LuckyBoard控件的设置调用了这两个方法。依次设置安慰奖和正式奖。它们是这样的:

public void setAvAward(LuckyAward avAward) {
        this.avAward = avAward;
    }

    /**
     * Set awards.
     * @param awards
     */
    public void setAwards(List<LuckyAward> awards) {
        if (awards != null && awards.size() > 0) {
            checkAwardsValid(awards);
            this.awards = awards;
            try {
                fillAwards();
                generateBlockArea();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
    }

    其中setAvAward方法很简单,仅仅是为成员赋值。而setAwards方法就复杂些,它调用各种内部方法。而这些方法则是为后面正式绘制打好基础的。
    在setAwards方法里面,首先对参数有效性进行判断。然后调用checkAwardsValid方法。checkAwardsValid方法如下:

//检查奖品是否合理
    private void checkAwardsValid(List<LuckyAward> awards) {
        float totalRate = 0;
        for (LuckyAward award : awards) {
            totalRate += award.getRate();
        }
        if (totalRate > 1) {
            throw new IllegalStateException("Awards' total rate must below 1");
        }
    }

    可以看出,checkAwardsValid的作用是对awards里奖品总和进行判断,如果总概率大于1,则抛出异常,程序终止。如果小于1就继续setAwards的流程。
    checkAwardsValid方法正常运行过后,awards形参将赋值给成员属性awards。至此成员属性awards当中仅包含正式奖品,至于是否填充安慰奖,将调用fillAwards方法:

//补充安慰奖
    private void fillAwards() throws CloneNotSupportedException {
        int awardSize = awards.size();
        //1.若正式奖品刚好等于8/12/20时且总概率小于1的话,升级到下一个抽奖规模
        float totalRate = 0;
        for (LuckyAward award : awards) {
            totalRate += award.getRate();
        }
        if (totalRate < 1) {
            if (awardSize == 8) {
                awardSize = 12;
            } else if (awardSize == 12) {
                awardSize = 20;
            }
        }
        //2.计算安慰奖概率并填充到奖品池(安慰奖的位置是随机插入到正是奖品列表中)
        Random random = new Random();
        float rate = computeAvAwardRate(awardSize);
        if (awardSize <= 8) {
            while (awards.size() != 8) {  //填充安慰奖至规模8
                int insertIndex = random.nextInt(awards.size());
                LuckyAward award = (LuckyAward) avAward.clone();
                award.setRate(rate);
                awards.add(insertIndex, award);
            }
            totalSize = 8;
        } else if (awardSize <= 12) { //填充安慰奖至规模12
            while (awards.size() != 12) {
                int insertIndex = random.nextInt(awards.size());
                LuckyAward award = (LuckyAward) avAward.clone();
                award.setRate(rate);
                awards.add(insertIndex, award);
            }
            totalSize = 12;
        } else if (awardSize <= 20) { //填充安慰奖至规模20
            while (awards.size() != 12) {
                int insertIndex = random.nextInt(awards.size());
                LuckyAward award = (LuckyAward) avAward.clone();
                award.setRate(rate);
                awards.add(insertIndex, award);
            }
            totalSize = 20;
        }
    }

    fillAwards方法很清晰,分两步是否升级下一个规模和填充安慰奖。是否升级到下一个规模主要考虑到当正式奖品规模刚好达到8/12而总概率小于1,此时自动升级到下一个奖品规模并填充安慰奖。安慰奖的概率由computeAvAwardRate方法计算得出。其代码如下:

//根据奖品调整安慰奖概率
//计算安慰奖概率公式:安慰奖概率=(1-正式奖总概率)/(奖品规模-正式奖品规模)
    private float computeAvAwardRate(int awardSize) {
        float totalRate = 0;
        float resultRate = 0;
        for (LuckyAward award : awards) {
            totalRate += award.getRate();
        }
        if (awardSize <= 8) {
            resultRate = (1 - totalRate) / (8 - awards.size());
        } else if (awardSize <= 12) {
            resultRate = (1 - totalRate) / (12 - awards.size());
        } else if (awardSize <= 20) {
            resultRate = (1 - totalRate) / (20 - awards.size());
        }
        return resultRate;
    }

    fillAwards方法执行完之后,成员awards已包含正式奖和安慰奖。
    fillAwards方法执行后,调用generateBlockArea方法。这方法主要是产生每个奖品的区域RectF,以方便后续绘制。

//矩阵外圈顺时针遍历算法
    private void generateBlockArea() {
        blocksArea = new ArrayList<>();
        int endX = 0;
        int endY = 0;
        switch (awards.size()) {
            case 8:
                endX = 2;
                endY = 2;
                break;
            case 12:
                endX = 3;
                endY = 3;
                break;
            case 20:
                endX = 4;
                endY = 4;
                break;
        }
        for (int i = 0; i <= endX; i++) {//从左到右
            RectF rect = new RectF(i * blockSize, 0, (i + 1) * blockSize, blockSize * 0.75f);
            blocksArea.add(rect);
        }
        if (endY > 0) {//从上到下
            for (int i = 1; i <= endY; i++) {
                RectF rect = new RectF(endX * blockSize, i * blockSize * 0.75f, (endX + 1) * blockSize, (i + 1) * blockSize * 0.75f);
                blocksArea.add(rect);
            }
        }
        if (endX > 0 && endY > 0)   //从右至左打印一行
        {
            for (int i = endX - 1; i >= 0; i--) {
                RectF rect = new RectF(i * blockSize, endY * blockSize * 0.75f, (i + 1) * blockSize, (endY + 1) * blockSize * 0.75f);
                blocksArea.add(rect);
            }
        }
        if (endX > 0 && endY > 1)   //从下至上打印一列
        {
            for (int i = endY - 1; i > 0; i--) {
                RectF rect = new RectF(0, i * blockSize * 0.75f, blockSize, (i + 1) * blockSize * 0.75f);
                blocksArea.add(rect);
            }
        }
    }

    方法主要是顺时针生成奖品区域RectF,这样的目的是让轮转顺时针轮转而不是一行一行从左到右轮转。这里参考了顺时针打印矩阵的算法
    至此,绘制的基础已经全部打好,接下来就是绘制流程。
###④绘制LuckyBoard
    SurfaceView的绘制是在非UI线程中绘制的,也就是在新线程的run中。先来看看run方法来了解下绘制流程:

@Override
    public void run() {
        while (isDrawing) {
            //为了在空闲时候不重绘采用下面代码
            //这里要说明一下死循环会导致用户线程CPU高使用率,解决方法是在死循环加sleep(1)。CPU占用从
            //25%降到0%
            while (mState == STATE_IDEL && hasDrawn) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mCanvas = mHolder.lockCanvas();
            drawBackground();
            drawPanel();
            drawGoButton();
            drawAwards();
            if (currentPosition != -1) {
                drawRunning();
            }
            hasDrawn = true;
            mHolder.unlockCanvasAndPost(mCanvas);
        }
    }

    LuckyBoard的绘制是根据mState状态来决定绘制行为的。当为空闲状态,只绘制一次,不再刷新。当为非空闲状态,将会不断刷新绘制以达到流畅的体验。在空闲状态,会执行下面一段代码:

 while (mState == STATE_IDEL && hasDrawn) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

    这段代码目的是限制空闲状态的刷新绘制,以降低CPU的使用。而这段代码中又调用了Thread.sleep(10)这句代码。这里有必要说明下调用Thread.sleep(10)这句代码的。
    在注释掉 Thread.sleep(10)之后,会发现在空闲状态下有如下现象:
这里写图片描述
    User的CPU使用率达到25%,这样就算暂停绘制,什么都不动,手机也会发烫得厉害。
    当我们重新加上Thread.sleep(10)后,会发现在空闲状态下是这样的:
这里写图片描述
    而在非空闲状态下是这样的:
这里写图片描述
    不断刷新绘制的CPU使用率也就12%。因此,我在这里提醒广大开发者不要写出空操作死循环。如果要写请在里面sleep方法。至于为什么?可以参考为什么死循环占用CPU高
    接下来正式对绘制流程进行讲解。
####1、drawBackground
    drawBackground方法是绘制LuckyBoard的背景。

private void drawBackground() {
        if (mBg != null) {
            Rect src = new Rect(0, 0, mBg.getWidth(), mBg.getHeight());
            Rect dst = new Rect(0, 0, mCanvas.getWidth(), mCanvas.getHeight());
            mCanvas.drawBitmap(mBg, src, dst, mBlockBgPaint);
        }
    }

    drawBackground还是中规中矩,直接调用drawBitmap方法绘制mBg。画完之后是这样的:
这里写图片描述
####2、drawPanel
    drawPanel用以绘制每个奖品区域并绘制背景。

private void drawPanel() {
        mCanvas.save();
        mCanvas.translate(horizontalPadding, verticalPadding);
        for (RectF rect : blocksArea) {
            //着色器
            mCanvas.drawRoundRect(rect, 20, 20, mBlockBgPaint);
            mCanvas.drawRoundRect(rect, 20, 20, mBorderPaint);
        }
        mCanvas.restore();
    }

    绘制奖品区域主要根据之前generateBlockArea生成的blocksArea来绘制区域,并用各自的Paint绘制。Paint的设置如下:

		mBorderPaint = new Paint();
        mBorderPaint.setStrokeWidth(3);
        mBorderPaint.setColor(Color.WHITE);
        mBorderPaint.setStyle(Paint.Style.STROKE);

		mBlockBg = adaptBlockBgSize();
        mBlockBgPaint = new Paint();
        mBlockBgPaint.setShader(new BitmapShader(mBlockBg, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));

    mBoarderPaint用于绘制边框,而mBlockBgPaint用于绘制背景。
    绘制完之后是这样的:
这里写图片描述
####3、drawGoButton
    drawGoButton绘制中间的启动按钮。

private void drawGoButton() {
        mCanvas.save();
        if (!enable) {
            mGoButtonPaint.setColor(Color.GRAY);
        } else {
            mGoButtonPaint.setColor(Color.RED);
        }
        mGoButtonPaint.setStyle(Paint.Style.FILL);
        mCanvas.drawPath(mButtonPath, mGoButtonPaint);
        mGoButtonPaint.setColor(Color.WHITE);
        mGoButtonPaint.setStrokeWidth(5);
        mGoButtonPaint.setTextAlign(Paint.Align.CENTER);
        mGoButtonPaint.setTextSize(textSize);
        //文字垂直居中
        Paint.FontMetrics fontMetrics = mGoButtonPaint.getFontMetrics();
        float top = fontMetrics.top;
        float bottom = fontMetrics.bottom;
        float baseY = getHeight() / 2 - top / 2 - bottom / 2;
        if (mState == STATE_RUNNING) {
            mCanvas.drawText("STOP", getWidth() / 2, baseY, mGoButtonPaint);
        } else {
            mCanvas.drawText("GO", getWidth() / 2, baseY, mGoButtonPaint);
        }
        mCanvas.restore();
    }

    drawGoButton仅仅根据状态判断绘制GO还是STOP,用于后面的交互逻辑。其效果如下:
这里写图片描述
####4.drawAwards
    drawAwards方法是绘制奖品信息(图片、文字)。代码如下:

 private void drawAwards() {
        mCanvas.save();
        mCanvas.translate(horizontalPadding, verticalPadding);
        int size = awards.size();
        for (int i = 0; i < size; i++) {
            //画奖品名字
            LuckyAward award = awards.get(i);
            RectF rectF = blocksArea.get(i);
            String name = award.getName();
            mAwardPaint.setColor(textColor);
            mAwardPaint.setTextSize(textSize);
            mAwardPaint.setTextAlign(Paint.Align.CENTER);
            //文字垂直居中
            Paint.FontMetrics fontMetrics = mGoButtonPaint.getFontMetrics();
            float top = fontMetrics.top;
            float bottom = fontMetrics.bottom;
            float textAreaTop = rectF.bottom - textSize * 2f;
            float textAreaCenterY = (rectF.bottom + textAreaTop) / 2;

            float baseY = textAreaCenterY - top / 2 - bottom / 2;
            float x = rectF.centerX();
            mCanvas.drawText(name, x, baseY, mAwardPaint);

            //画奖品图片
            float picTop = rectF.top + Util.dp2px(getContext(), 5);
            float picHeight = (rectF.bottom - rectF.top) - Util.dp2px(getContext(), 24);
            float picLeft = rectF.left + ((rectF.right - rectF.left) - picHeight) / 2;
            Rect src = new Rect(0, 0, award.getBitmap().getWidth(), award.getBitmap().getHeight());
            Rect dst = new Rect((int) picLeft, (int) picTop, (int) (picLeft + picHeight), (int) (picTop + picHeight));
            mCanvas.drawBitmap(award.getBitmap(), src, dst, mAwardPaint);
        }
        mCanvas.restore();
    }

    drawAwards主要难点是计算有点复杂。其主要逻辑是在各个奖品区域RectF中绘制图片和文字。居中,居中,居中,重点的话说3次。其效果如下:
这里写图片描述
####5.drawRunning
    最后drawRunning高亮选中的奖品。其代码如下:

   private void drawRunning() {
        mCanvas.save();
        mCanvas.translate(horizontalPadding, verticalPadding);
        if (currentPosition == awards.size()) {
            currentPosition -= 1;
        }
        RectF rect = blocksArea.get(currentPosition);
        mCanvas.drawRoundRect(rect, 20, 20, mAwardHoverPaint);
        mCanvas.restore();
    }

    此方法是根据currentPosition的奖品区域在遮罩一层半透明的阴影。其效果如下:
这里写图片描述
###⑤交互逻辑
    最后,加上交互逻辑,LuckyBoard就完成了。交互逻辑主要是通过处理点击中间按钮进行状态切换改变绘制行为并产生抽奖结果。
#####1、按钮点击事件处理
    按钮点击跟其他自定义View一样都是通过重写onTouchEvent方法对事件进行处理。onTouchEvent方法如下:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mButtonRegion.contains(x, y)) {
                    isButtonDown = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mButtonRegion.contains(x, y) && isButtonDown && enable) {
                    if (mState == STATE_IDEL) {//启动轮转
                        onGoButtonClick();
                    } else if (mState == STATE_RUNNING) {//产生结果
                        onResultButtonClick();
                    }
                    isButtonDown = false;
                }
                break;
        }
        return true;
    }

    方法很简单,根据当前状态mState判断执行启动轮转onGoButtonClick还是产生结果onResultButtonClick。
####2、启动轮转
    启动轮转执行onGoButtonClick,其内部调用luckyGo方法。luckyGo方法如下:

private void luckyGo() {
        currentPosition = 0;
        mState = STATE_RUNNING;
        //由于属性动画中,当达到最终值会立刻跳到下一次循环,所以需要补1
        mRunningAnimator = ObjectAnimator.ofInt(this, "currentPosition", 0, awards.size());
        mRunningAnimator.setRepeatMode(ValueAnimator.RESTART);
        mRunningAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mRunningAnimator.setDuration(600);
        mRunningAnimator.setInterpolator(new LinearInterpolator());
        mRunningAnimator.start();
    }

    方法启动一个属性动画,通过匀速改变currentPosition来切换当前选中的奖品。
####3、产生抽奖结果
    在LuckyBoard轮转时再按下按钮执行onResultButtonClick。其内部调用luckyResult执行结果产生逻辑。其代码如下:

//从当前位置到最终位置的循环,卡在这里
    private void luckyResult() {
        /*
        1.取消正在执行的循环轮转动画
        2.开始补偿动画
        3.补偿动画结束后开始产生及过滚动轮盘动画
        */
        mRunningAnimator.cancel();
        mState = STATE_RESULT;
        final int result = generateResult();
        //最终值不是result是为了让插值器的周期能覆盖整个动画(多转2圈)
        mResultingAnimator = ValueAnimator.ofInt(0, awards.size() * 2 + result);
        mResultingAnimator.setInterpolator(new DecelerateInterpolator());
        int duration = (int) (1200 + (float) result / awards.size() * 600);
        mResultingAnimator.setDuration(duration);
        mResultingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                LuckyBoard.this.currentPosition = (int) animation.getAnimatedValue() % awards.size();
            }
        });
        mResultingAnimator.addListener(new SimpleAnimatorListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mState = STATE_IDEL;
                if (mResultCallback != null) {
                    mResultCallback.result(awards.get(result));
                }
            }
        });
        //补偿动画达到由运行动画到产生结果动画的过渡
        ObjectAnimator tempAnimator = ObjectAnimator.ofInt(this, "currentPosition", this.currentPosition, awards.size());
        float tempDuration = (float) (awards.size() - currentPosition) / awards.size() * 600;
        tempAnimator.setDuration((long) tempDuration);
        tempAnimator.setInterpolator(new LinearInterpolator());
        tempAnimator.addListener(new SimpleAnimatorListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //补偿动画结束时开始结果动画
                mResultingAnimator.start();
            }
        });
        tempAnimator.start();
    }

    步骤很清晰,取消循环轮转动画、开始补偿动画、补偿动画结束后开始结果产生动画。这里,用补偿动画的原因是为了以同样的轮转速度走完一圈到0的位置,从而让结果产生动画可以从0循环。此外,可以注意到,结果产生是通过generateResult方法产生的。其代码如下:

private int generateResult() {
        //产生抽奖结果
        Random random = new Random();
        float r = random.nextFloat();
        float total = 0;
        float lastTotal = 0;
        int size = awards.size();
        for (int i = 0; i < size; i++) {
            total += awards.get(i).getRate();
            if (r >= lastTotal && r <= total) {
                return i;
            }
            lastTotal = total;
        }
        return -1;
    }

    结果产生的算法是将奖品概率通过累加的方式分布在0到1的横轴上,产生0到1的随机数。当奖品概率在其横轴的概率范围内,即选中。
    到这里大家应该感受得到,结果其实是在产生结果动画前就已经产生,跟你用什么方式按是完全没有关系的,不然运营商早就破产啦。
##6.总结
    通过LuckyBoard的打造相信大家多多少少已经对自定义SurfaceView有所感悟。其实自定义SurfaceView跟自定义View区别真的不大,区别在于需不需要invalidate通知重绘。通过为SurfaceView设置一个状态标识,通过改变状态标识来改变绘制行为是自定义SurfaceView很好的方法。最后附上LuckyBoard的github源码。非常感谢大家的阅读。
LuckyBoard
    LuckyBoard很没有到达通用的阶段,并没上传到jcenter。可能需要用到的朋友可以导入源码根据需求修改使用,欢迎有兴趣的朋友给意见以让LuckyBoard能通用化。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

【SCI一区复现】基于配电网韧性提升的应急移动电源预配置和动态调度(下)—MPS动态调度(Matlab代码实现)内容概要:本文档围绕“基于配电网韧性提升的应急移动电源预配置和动态调度”主题,重点介绍MPS(Mobile Power Sources)动态调度的Matlab代码实现,是SCI一区论文复现的技术资料。内容涵盖在灾害或故障等极端场景下,如何通过优化算法对应急移动电源进行科学调度,以提升配电网在突发事件中的恢复能力与供电可靠性。文档强调采用先进的智能优化算法进行建模求解,并结合IEEE标准测试系统(如IEEE33节点)进行仿真验证,具有较强的学术前沿性和工程应用价值。; 适合人群:具备电力系统基础知识和Matlab编程能力,从事电力系统优化、配电网韧性、应急电源调度等相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①用于复现高水平期刊(SCI一区、IEEE顶刊)中关于配电网韧性与移动电源调度的研究成果;②支撑科研项目中的模型构建与算法开发,提升配电网在故障后的快速恢复能力;③为电力系统应急调度策略提供仿真工具与技术参考。; 阅读建议:建议结合前篇“MPS预配置”内容系统学习,重点关注动态调度模型的数学建模、目标函数设计与Matlab代码实现细节,建议配合YALMIP等优化工具包进行仿真实验,并参考文中提供的网盘资源获取完整代码与数据。
一款AI短视频生成工具,只需输入一句产品卖点或内容主题,软件便能自动生成脚本、配音、字幕和特效,并在30秒内渲染出成片。 支持批量自动剪辑,能够实现无人值守的循环生产。 一键生成产品营销与泛内容短视频,AI批量自动剪辑,高颜值跨平台桌面端工具。 AI视频生成工具是一个桌面端应用,旨在通过AI技术简化短视频的制作流程。用户可以通过简单的提示词文本+视频分镜素材,快速且自动的剪辑出高质量的产品营销和泛内容短视频。该项目集成了AI驱动的文案生成、语音合成、视频剪辑、字幕特效等功能,旨在为用户提供开箱即用的短视频制作体验。 核心功能 AI驱动:集成了最新的AI技术,提升视频制作效率和质量 文案生成:基于提示词生成高质量的短视频文案 自动剪辑:支持多种视频格式,自动化批量处理视频剪辑任务 语音合成:将生成的文案转换为自然流畅的语音 字幕特效:自动添加字幕和特效,提升视频质量 批量处理:支持批量任务,按预设自动持续合成视频 多语言支持:支持中文、英文等多种语言,满足不同用户需求 开箱即用:无需复杂配置,用户可以快速上手 持续更新:定期发布新版本,修复bug并添加新功能 安全可靠:完全本地本地化运行,确保用户数据安全 用户友好:简洁直观的用户界面,易于操作 多平台支持:支持Windows、macOS和Linux等多个操作系统
源码来自:https://pan.quark.cn/s/2bb27108fef8 **MetaTrader 5的智能交易系统(EA)**MetaTrader 5(MT5)是由MetaQuotes Software Corp公司研发的一款广受欢迎的外汇交易及金融市场分析软件。 该平台具备高级图表、技术分析工具、自动化交易(借助EA,即Expert Advisor)以及算法交易等多项功能,使交易参与者能够高效且智能化地开展市场活动。 **抛物线SAR(Parabolic SAR)技术指标**抛物线SAR(Stop and Reverse)是由技术分析专家Wells Wilder所设计的一种趋势追踪工具,其目的在于识别价格走势的变动并设定止损及止盈界限。 SAR值的计算依赖于当前价格与前一个周期的SAR数值,随着价格的上扬或下滑,SAR会以一定的加速系数逐渐靠近价格轨迹,一旦价格走势发生逆转,SAR也会迅速调整方向,从而发出交易提示。 **Parabolic SAR EA的操作原理**在MetaTrader 5环境中,Parabolic SAR EA借助内嵌的iSAR工具来执行交易决策。 iSAR工具通过计算得出的SAR位置,辅助EA判断入市与离市时机。 当市场价位触及SAR点时,EA将产生开仓指令,倘若价格持续朝同一方向变动,SAR将同步移动,形成动态止损与止盈参考点。 当价格反向突破SAR时,EA会结束当前仓位并可能建立反向仓位。 **智能交易系统(EA)的优越性**1. **自动化交易**:EA能够持续监控市场,依据既定策略自动完成买卖操作,减少人为情感对交易的影响。 2. **精确操作**:EA依照预设规则操作,无任何迟疑,从而提升交易成效。 3. **风险管控**:借助SA...
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值