Android 抽獎轉盤的實現

Nipuream發表於2016-08-19

** 本篇文章已授權公眾號 guolin_blog (郭霖)獨家釋出 **

序言

最近需要實現一個抽獎的控制元件,我簡單搜尋了下,感覺要不很多細節地方沒有處理,要麼,根本就不能用。索性想自己實現個,從千圖網搜了下,挑選了個自己比較喜歡的出來,psd開啟後效果如下:


這裡寫圖片描述



最終實現效果如下:


點選Go按鈕自動滾動:


這裡寫圖片描述



隨手勢滾動:


這裡寫圖片描述



實現的效果還不錯,因為是模擬器加錄製,畫面可能會有些卡頓,真機其實蠻順暢的,下面簡單的講講實現的步驟。


實現

  • 1,繪製。

    首先第一個我們要它給畫出來,但是要注意的就是Android所對應的座標系的問題。


    這裡寫圖片描述

   for(int i= 0;i<6;i++){
            if(i%2 == 0){
                canvas.drawArc(rectF,angle,60,true,dPaint);
            }else
            {
                canvas.drawArc(rectF,angle,60,true,sPaint);
            }
            angle += 60;
        }

        for(int i=0;i<6;i++){
            drawIcon(width/2, height/2, radius, InitAngle, i, canvas);
            InitAngle += 60;
        }

        for(int i=0;i<6;i++){
            drawText(InitAngle+30,strs[i], 2*radius, textPaint, canvas,rectF);
            InitAngle += 60;
        }

其中有兩個地方需要注意下,第一個就是畫弧的地方第一個角度是起始角度,第二個是弧的角度,並不是結束的角度,所以是固定值60。第二個地方就是計算具體的x,y的值的時候要根據弧度去計算,不能根據角度。

  • 2.使用屬性動畫讓其自動旋轉。

如果用SurfaceView去進行重繪旋轉存在一些問題,比如旋轉的角度不好控制,旋轉的速度不好控制。但是用屬性動畫,這個問題就很好解決了。

  ValueAnimator animtor = ValueAnimator.ofInt(InitAngle,DesRotate);
        animtor.setInterpolator(new AccelerateDecelerateInterpolator());
        animtor.setDuration(time);
        animtor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int updateValue = (int) animation.getAnimatedValue();
                InitAngle = (updateValue % 360 + 360) % 360;
                ViewCompat.postInvalidateOnAnimation(RotatePan.this);
            }
        });
        animtor.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);

                int pos = InitAngle / 60;

                if(pos >= 0 && pos <= 3){
                    pos = 3 - pos;
                }else{
                    pos = (6-pos) + 3;
                }

                if(l != null)
                    l.endAnimation(pos);
            }
        });
        animtor.start();

用動畫最重要的就是,如何計算出結束動畫後的位置,那麼把最終旋轉的總角度%360°就得到最後一圈實際旋轉的角度,再除以60就得到了到底選擇了幾個位置,因為一個位置佔據60°,這應該不難理解。

    @Override
        public void onAnimationEnd(Animation animation) {
            int pos = startDegree % 360 / 60;

            if(pos >= 0 && pos <= 3){
                pos = 3 - pos;
            }else{
                pos = (6-pos) + 3;
            }

            if(l != null)
                l.endAnimation(pos);
        }

但是問題又來了,Android所對應的座標系,0的位置應該是最底下,而指標的位置是在最上面,所以,我們結合上面的座標系來看,還需要處理下,如上面的程式碼所示。

  • 3.利用Scroller和GestureDetector對手勢進行處理。

    觸控事件的處理,最後到底允不允許轉盤隨手勢滑動呢?其實貌似做成這樣也就可以了,但是最後還是實現了下,用到了GestureDetector 和 Scroller這個類。其實做法有很多,首先獲取我們的滑動的距離,Math.sqrt(dx * dx + dy * dy),然後無非就是把這個距離轉換成我們需要的角度,你可以把這個距離當作我們的周長來處理,也可以把這個距離當作我們總的旋轉的角度來處理。之後就是隨著時間的流逝,不斷的重新整理我們的介面了。

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean consume = mDetector.onTouchEvent(event);
        if(consume)
        {
            getParent().requestDisallowInterceptTouchEvent(true);
            return true;
        }

        return super.onTouchEvent(event);
    }


    public void setRotate(int rotation){
        rotation = (rotation % 360 + 360) % 360;
        InitAngle = rotation;
        ViewCompat.postInvalidateOnAnimation(this);
    }


    @Override
    public void computeScroll() {

        if(scroller.computeScrollOffset()){
            setRotate(scroller.getCurrY());
        }

        super.computeScroll();
    }

    private class RotatePanGestureListener extends GestureDetector.SimpleOnGestureListener{

        @Override
        public boolean onDown(MotionEvent e) {
            return super.onDown(e);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            float centerX = (RotatePan.this.getLeft() + RotatePan.this.getRight())*0.5f;
            float centerY = (RotatePan.this.getTop() + RotatePan.this.getBottom())*0.5f;

            float scrollTheta = vectorToScalarScroll(distanceX, distanceY, e2.getX() - centerX, e2.getY() -
                    centerY);
            int rotate = InitAngle -
                    (int) scrollTheta / FLING_VELOCITY_DOWNSCALE;

            setRotate(rotate);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            float centerX = (RotatePan.this.getLeft() + RotatePan.this.getRight())*0.5f;
            float centerY = (RotatePan.this.getTop() + RotatePan.this.getBottom())*0.5f;

            float scrollTheta = vectorToScalarScroll(velocityX, velocityY, e2.getX() - centerX, e2.getY() -
                    centerY);
            scroller.abortAnimation();
            scroller.fling(0, InitAngle , 0, (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
                    0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
            return true;
        }

    }

    private float vectorToScalarScroll(float dx, float dy, float x, float y) {
        // get the length of the vector
        float l = (float) Math.sqrt(dx * dx + dy * dy);

        // decide if the scalar should be negative or positive by finding
        // the dot product of the vector perpendicular to (x,y).
        float crossX = -y;
        float crossY = x;

        float dot = (crossX * dx + crossY * dy);
        float sign = Math.signum(dot);

        return l * sign;
    }


  • 4.剩餘問題處理

    還存在個問題,如果沒有手勢去操作轉盤,那我們很容易判斷它所旋轉的角度,但是有手勢的參與,我們很容易旋轉到轉盤中兩個分片中間的位置,那麼,我們在讓它旋轉之前,要簡單處理下,避免這種事情發生。

   //TODO 為了每次都能旋轉到轉盤的中間位置
        int offRotate = DesRotate % 360 % 60;
        DesRotate -= offRotate;
        DesRotate += 30;

這樣不管手勢怎麼操作,我最終都是旋轉到分片的中間位置了。


  • 5.轉動到指定某個區域
   /**
     * 開始轉動
     * @param pos 如果 pos = -1 則隨機,如果指定某個值,則轉到某個指定區域
     */
    public void startRotate(int pos){

        int lap = (int) (Math.random()*12) + 4;

        int angle = 0;
        if(pos < 0){
            angle = (int) (Math.random() * 360);
        }else{
            int initPos  = queryPosition();
            if(pos > initPos){
                angle = (pos - initPos)*60;
                lap -= 1;
                angle = 360 - angle;
            }else if(pos < initPos){
                angle = (initPos - pos)*60;
            }else{
                //nothing to do.
            }
        }

好多人加我QQ,問我怎麼轉動到指定位置,所以更新了下程式碼,上傳到github上去了,這裡做個日誌。如果傳的是 -1 則隨機轉動,如果傳的是大於0,則轉動到指定位置。


  • 6.改變轉盤數量
   <com.hr.nipuream.luckpan.view.RotatePan
        android:id="@+id/rotatePan"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="78dp"
        android:layout_centerHorizontal="true"
        luckpan:pannum="8"
        luckpan:names="@array/names"
        luckpan:icons="@array/icons"
        />


        <resources>
            <string-array name="names">
                <item>action</item>
                <item>adventure</item>
                <item>combat</item>
                <item>moba</item>
                <item>other</item>
                <item>role</item>
                <item>sports</item>
                <item>words</item>
            </string-array>

            <string-array name="icons">
                <item>action</item>
                <item>adventure</item>
                <item>combat</item>
                <item>moba</item>
                <item>other</item>
                <item>role</item>
                <item>sports</item>
                <item>words</item>
            </string-array>
        </resources>

將pannum改為你想要的數量,然後names和icons定義在arrays.xml檔案中, 其中arrays.xml中的數量要和轉盤的數量一致。理論上可以改為轉盤數量為N的情況,但是綜合來看還是6個和8個轉盤數量最適宜,而且很多人也只問我怎麼改成8個轉盤,所以對這兩種情況做了適配,如果後期還有別的需求在加吧,github上程式碼已經更新,請重新下載。

程式碼

最後,程式碼已經上傳到github上去了。地址:https://github.com/Nipuream/LuckPan         歡迎Star

LuckPan.apk        apk下載地址

相關文章