Android學習筆記之如何使用圓形菜單實現旋轉效果...

jopen 8年前發布 | 27K 次閱讀 安卓開發 Android開發 移動開發

Android學習筆記之如何使用圓形菜單實現旋轉效果...

學習內容:

1.使用圓形菜單并實現旋轉效果..

Android的圓形菜單我也是最近才接觸到,由于在界面中確實是使用到了,因此就去學習了一下圓形菜單的使用,并且實現菜單的旋轉效果,類似于摩天輪那樣的效果,個人感覺還是蠻不錯的,就是在實現的過程中有點麻煩...通過動態加載的方式,使用ViewGroup來實現了這個過程...個人感覺是一個蠻復雜的過程,最后附上源碼...上面先附上效果圖,其實這個圖給人的感覺更像是摩天輪,轉動中心位置的時候,幾個附帶的小圓會進行轉動,通過轉動我們可以看到被擋住的部分...算是為了實現一種互動吧...直接上實現過程的代碼,然后在進行解釋...估計看到下面的代碼,很多人就惡心了...但是如果堅持看完了,那么就真正學會了...先不要看下面的代碼,把代碼先過掉...看下面的解釋...

package com.circle.cil_212_app;
import com.example.cil_212_app.R;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@TargetApi(Build.VERSION_CODES.CUPCAKE)
public class CircleMenuLayout extends ViewGroup{
    //→_→ 第一部分...
    private int mRadius;   //定義layout的半徑...
    //定義了兩個數值...在選取半徑長度的時候需要使用...
    private float mMaxChildDimesionRadio = 1/ 4f;
    private float mCenterItemDimesionRadio = 1/ 3f;
    private LayoutInflater inflater;  //自定義加載布局...
    private double StartAngle;   //定義初始角度...
    private String[] ItemTexts = new String[]{"HTML", "CSS", "JS",
            "JQuery", "DOM", "TEMPLETE"};
    private int[] ItemImgs = new int[]{R.drawable.home_mbank_1_normal,R.drawable.home_mbank_2_normal,R.drawable.home_mbank_3_normal,R.drawable.home_mbank_4_normal,R.drawable.home_mbank_5_normal,R.drawable.home_mbank_6_normal};
    private int TouchSlop;
    /*
     * 加速度檢測
     * */
    private float DownAngle;
    private float TmpAngle;
    private long DownTime;
    private boolean isFling;  //判斷是否在自由旋轉...
    //→_→  第二部分..

    public CircleMenuLayout(Context context, AttributeSet attrs) {
        super(context,attrs);
        // TODO Auto-generated constructor stub

        inflater=LayoutInflater.from(context);

        for(int i=0;i<ItemImgs.length;i++){
            final int j=i;

            View view=inflater.inflate(R.layout.web_circle_1, this, false); 

            ImageView iv=(ImageView) view.findViewById(R.id.id_circle_menu_item_image);
            TextView tv=(TextView) view.findViewById(R.id.id_circle_menu_item_text);
            iv.setImageResource(ItemImgs[i]);
            tv.setText(ItemTexts[i]);

            view.setOnClickListener(new OnClickListener() {
                @SuppressLint("NewApi")
                @Override
                public void onClick(View v) {
                    // TODO Auto-generated method stub
                    Toast.makeText(getContext(), ItemTexts[j], Toast.LENGTH_SHORT).show();
                }
            });
            addView(view);
        }

        TouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    }
    //→_→ 第三部分

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){

        setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight());

        mRadius=Math.max(getWidth(), getHeight());
        final int count =getChildCount();  
        int childsize=(int)(mRadius*mMaxChildDimesionRadio);
        int childMode=MeasureSpec.EXACTLY;
        for(int i=0;i<count;i++){

            final View child=getChildAt(i);

            if(child.getVisibility()==GONE){
                continue;
            }
            int makeMeasureSpec=-1;

            if(child.getId()==R.id.id_circle_menu_item_center){

                makeMeasureSpec = MeasureSpec.makeMeasureSpec(
                        (int) (mRadius * mCenterItemDimesionRadio), childMode);
            }else{
                makeMeasureSpec=MeasureSpec.makeMeasureSpec(childsize, childMode);
            }

            child.measure(makeMeasureSpec, makeMeasureSpec);
        }
    }
    //→_→ 第四部分
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // TODO Auto-generated method stub
        int layoutWidth = r - l;
        int layoutHeight = b - t;

        int layoutRadius = Math.max(layoutWidth, layoutHeight);
        final int childCount=getChildCount();
        int left,top;
        int radius = (int) (layoutRadius * mMaxChildDimesionRadio);

        float angleDelay = 360 / (getChildCount() - 1);
        for (int i = 0; i < childCount; i++)
        {
            final View child = getChildAt(i);
            if (child.getId() == R.id.id_circle_menu_item_center)
                continue;
            if (child.getVisibility() == GONE){
                continue;
               }

            StartAngle %= 360;
            float tmp = layoutRadius * 1f / 3 - 1 / 22f * layoutRadius;
            left = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                            * Math.cos(Math.toRadians(StartAngle)) - 1 / 2f
                            * radius);
            top = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                            * Math.sin(Math.toRadians(StartAngle)) - 1 / 2f
                            * radius);

            child.layout(left, top, left + radius, top + radius);
            StartAngle += angleDelay;
        }
        View cView = findViewById(R.id.id_circle_menu_item_center);
        cView.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v){
                Toast.makeText(getContext(), "aa", Toast.LENGTH_LONG).show();
            }
        });

        int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;
        int cr = cl + cView.getMeasuredWidth();
        cView.layout(cl, cl, cr, cr);
    }
    //→_→ 第五部分
    private float mLastX;
    private float mLastY;
    private FlingRunnable mFlingRunnable;
    @Override
    public boolean dispatchTouchEvent(MotionEvent event){

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
        //按下操作...事件機制自帶的變量...
        case MotionEvent.ACTION_DOWN:
            mLastX = x;
            mLastY = y;
            DownAngle = getAngle(x, y);
            DownTime = System.currentTimeMillis();  
            TmpAngle = 0;
            if (isFling){
                removeCallbacks(mFlingRunnable);
                isFling = false;
                return true ; 
            }
            break;

        case MotionEvent.ACTION_MOVE:

            float start = getAngle(mLastX, mLastY);
            float end = getAngle(x, y);
            if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4){

                StartAngle += end - start;
                TmpAngle += end - start;
            }else{
                StartAngle += start - end;
                TmpAngle += start - end;
            }

            requestLayout();

            mLastX = x;
            mLastY = y;
            break;

        case MotionEvent.ACTION_UP:

            float anglePrMillionSecond = TmpAngle * 1000
                    / (System.currentTimeMillis() - DownTime);

            if (Math.abs(anglePrMillionSecond) > 230 && !isFling){

                post(mFlingRunnable = new FlingRunnable(anglePrMillionSecond));
            }
            if(Math.abs(anglePrMillionSecond) >230 || isFling){
                return true ; 
            }
            break;
        }
        return super.dispatchTouchEvent(event);
    }

    private float getAngle(float xTouch, float yTouch){
        double x = xTouch - (mRadius / 2d);
        double y = yTouch - (mRadius / 2d);
        return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
    }

    private int getQuadrant(float x, float y){
        int tmpX = (int) (x - mRadius / 2);
        int tmpY = (int) (y - mRadius / 2);
        if (tmpX >= 0){
            return tmpY >= 0 ? 4 : 1;
        }else{
            return tmpY >= 0 ? 3 : 2;
        }
    }
    //→_→ 第六部分
    private class FlingRunnable implements Runnable{
        private float velocity;
        public FlingRunnable(float velocity){
            this.velocity = velocity;
        }
        public void run(){
            if ((int) Math.abs(velocity) < 20){
                isFling = false;
                return;
            }

            isFling = true;
            StartAngle += (velocity / 30);
            velocity /= 1.0666F;
            postDelayed(this, 30);
            requestLayout();
        }
    }
}

在這里開始進行正式的解釋,我把代碼分成了六個部分...這六個部分最核心的地方就屬于第三部分和第四部分和第五部分了,因此我就先解釋三四五部分...首先是第三個部分,第三個部分是測量部分,因為這個布局中,這七個彩色圓圈圖需要我們動態的添加到我們的布局上,為什么要動態布局,而不是靜態,因為后面我們需要實現旋轉效果,可能有人不明白為什么旋轉就要用動態加載布局,其實旋轉就是對布局的樣式進行持續刷新,每一次刷新都會導致控件的位置的改變,我們總不能把旋轉一圈的布局文件全寫完吧?因此動態的添加是為了有更高的靈活性..因此我們需要一個測量的過程,因為動態的加載控件時,系統需要對每一個控件的位置進行計算,找到一個合適的ViewGroup(ViewGroup我們可以把它理解成一個容器,這個容器可以放入組件,也可以放入子容器),然后將控件放入到其中,但是系統計算的數據往往不一定是我們想要的那樣...因此我在這里使用了第三部分進行計算...計算每一個控件需要多大的空間才能夠放得下...這就是第三部分的完整解釋...

//→_→ 第三部分
    //在動態加載布局的時候,我們需要人為的進行計算...布局的大小...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        /*
         * 調用這個方法的目的是為View設置大小...
         * 直接設置成系統根據父容器算出的一個推薦的最小值...
         * */
        setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight());
        //獲取半徑...
        mRadius=Math.max(getWidth(), getHeight());
        final int count =getChildCount(); //獲取子控件的數量,這里是7... 
        int childsize=(int)(mRadius*mMaxChildDimesionRadio);
        /* 這里涉及到了一個測量的模式,這個模式有三種屬性...
         * 
         * MeasureSpec.UNSPECIFIED,父視圖不對子視圖施加任何限制,子視圖可以得到任意想要的大小;
         *
         * MeasureSpec.EXACTLY,父視圖希望子視圖的大小是specSize中指定的大小;
         *  
           * MeasureSpec.AT_MOST,子視圖的大小最多是specSize中的大小。
         * */
        int childMode=MeasureSpec.EXACTLY;
        //對所有的子View進行迭代測量...說白了就是這7個View都得進行測量...
        for(int i=0;i<count;i++){
            final View child=getChildAt(i);
            //子控件不可顯示,直接跳過..
            if(child.getVisibility()==GONE){
                continue;
            }
            int makeMeasureSpec=-1;
            //子控件為中心圖標的時候,設置其半徑大小為1/3父容器半徑的大小...
            //如果子控件是其他,也就表示為周圍空間的時候,設置為父容器半徑的1/4大小...
            //測量的步驟在于下面的代碼塊...
            if(child.getId()==R.id.id_circle_menu_item_center){
                //此步驟是對數據和模式的一個封裝...返回一個48位的int值,高32位表示模式,低16位表示數值...
                makeMeasureSpec = MeasureSpec.makeMeasureSpec(
                        (int) (mRadius * mCenterItemDimesionRadio), childMode);
            }else{
                makeMeasureSpec=MeasureSpec.makeMeasureSpec(childsize, childMode);
            }
            //這個步驟才是真正的計算過程...傳遞過去的仍然是一個48位的值,系統會根據傳遞過去的模式來對View進行參數設置...
            child.measure(makeMeasureSpec, makeMeasureSpec);
        }
    }

第四部分其實就是正式進行布局了,但是布局我們不能隨便去布,否則不會出現想要的效果,這里涉及到了一個數學上的知識...中心位置的圓圈是很好測的,但是如何測中心圓旁邊的小圓位置才是關鍵..因此這里用了一個數學知識...

這就是如何測出中心圓圈到小圓圓心距離的測量方法...其實就是為了求出r和小圓圓心坐標...算出了這個坐標點的位置,這樣就可以進行放置了,這個坐標點的位置是一點點找出來的...需要進行嘗試,只是一個計算的過程,有心的人只要研究就能弄懂...無心的人給他解釋也是白搭...這就是第四部分的目的,就是為了算出位置...

//→_→ 第四部分
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // TODO Auto-generated method stub
        int layoutWidth = r - l;
        int layoutHeight = b - t;
        //對父容器進行布局...
        int layoutRadius = Math.max(layoutWidth, layoutHeight);
        final int childCount=getChildCount();
        int left,top;
        int radius = (int) (layoutRadius * mMaxChildDimesionRadio);
        //根據子元素的個數,設置角度...
        float angleDelay = 360 / (getChildCount() - 1);
        for (int i = 0; i < childCount; i++)
        {
            final View child = getChildAt(i);
            if (child.getId() == R.id.id_circle_menu_item_center)
                continue;
            if (child.getVisibility() == GONE){
                continue;
               }
            //取角度值..
            StartAngle %= 360;
            float tmp = layoutRadius * 1f / 3 - 1 / 22f * layoutRadius;
            left = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                            * Math.cos(Math.toRadians(StartAngle)) - 1 / 2f
                            * radius);
            top = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                            * Math.sin(Math.toRadians(StartAngle)) - 1 / 2f
                            * radius);
            // Log.e("TAG", "left = " + left + " , top = " + top);
            //由于前面還有1/8長度用來放置那個文本框...因此需要求出整體父容器的定位點...
            child.layout(left, top, left + radius, top + radius);
            StartAngle += angleDelay;
        }
        View cView = findViewById(R.id.id_circle_menu_item_center);
        cView.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v){
                Toast.makeText(getContext(), "aa", Toast.LENGTH_LONG).show();
            }
        });
        //設置中心...
        int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;
        int cr = cl + cView.getMeasuredWidth();
        cView.layout(cl, cl, cr, cr);
    }

第五部分和三四沒有關聯,第五部分是實現旋轉過程的一個重要過程...這里我重寫了dispatchTouchEvent來實現...這和上一篇是存在關聯的...第五部分其實就很簡單了,就是實現旋轉,旋轉時我們需要獲取旋轉的角度值和坐標值,獲取坐標值的目的是為了判斷角度值,角度值獲取到了后,傳遞給第六部分開啟線程...系統才會按照指定的角度對布局進行持續的刷新...

//→_→ 第五部分
    private float mLastX;
    private float mLastY;
    private FlingRunnable mFlingRunnable; //定義了一個線程...為實現第六部分定義了一個對象...

    @Override
    public boolean dispatchTouchEvent(MotionEvent event){
        //隨手指滑動特效...
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
        //按下操作...事件機制自帶的變量...
        case MotionEvent.ACTION_DOWN:
            mLastX = x;
            mLastY = y;
            DownAngle = getAngle(x, y);//獲取角度...
            DownTime = System.currentTimeMillis();  //系統的當前時間...
            TmpAngle = 0;
            //如果當前在進行快速滾動,那么移除對快速移動的回調...其實就是如果這個界面正在旋轉,在旋轉的期間我DOWN了一下,那么直接就停止旋轉...
            if (isFling){
                removeCallbacks(mFlingRunnable);
                isFling = false;
                return true ; 
            }
            break;
        //移動操作...
        case MotionEvent.ACTION_MOVE:
            //獲取開始和結束后的角度...我們這里移動的是角度...因此需要獲取角度...
            float start = getAngle(mLastX, mLastY);
            float end = getAngle(x, y);
            //判斷x,y的值是否在1,4象限...象限相比大家都明白,是為了獲取角度值...
            if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4){
             //在一四象限角度為正...
                StartAngle += end - start;
                TmpAngle += end - start;
            }else{
                StartAngle += start - end;
                TmpAngle += start - end;
            }
            //重新對布局進行設置...其實就是不斷刷新布局的一個過程...
            requestLayout();
            //將初始值設置為旋轉后的值...
            mLastX = x;
            mLastY = y;
            break;
        //抬起操作...
        case MotionEvent.ACTION_UP:
            //計算每秒鐘移動的角度...
            float anglePrMillionSecond = TmpAngle * 1000
                    / (System.currentTimeMillis() - DownTime);
            //如果數值大于這個指定的數值,那么就會認為是加速滾動...
            if (Math.abs(anglePrMillionSecond) > 230 && !isFling){
                //開啟一個新的線程,讓其進行自由滾動...
                post(mFlingRunnable = new FlingRunnable(anglePrMillionSecond));
            }
            if(Math.abs(anglePrMillionSecond) >230 || isFling){
                return true ; 
            }
            break;
        }
        return super.dispatchTouchEvent(event);
    }
    //這里用來測試旋轉的角度...算出角度之后返回...返回給上面的方法...
    private float getAngle(float xTouch, float yTouch){
        double x = xTouch - (mRadius / 2d);
        double y = yTouch - (mRadius / 2d);
        return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
    }
    //在這里我們對坐標值進行一個判斷..然后把坐標值返回...目的是測試角度...
    private int getQuadrant(float x, float y){
        int tmpX = (int) (x - mRadius / 2);
        int tmpY = (int) (y - mRadius / 2);
        if (tmpX >= 0){
            return tmpY >= 0 ? 4 : 1;
        }else{
            return tmpY >= 0 ? 3 : 2;
        }
    }

而第六部分其實就沒什么了,因為這個界面在旋轉的時候,我們不能讓他一直轉,遲早要停下來,因此我們需要有一個線程來幫助我們持續對界面刷新,并且控制刷新的速度,隨著velocity這個值越來越小刷新的速度也就越來越慢,最后就靜止不動,這樣給人的感覺就是一個減速的過程...

//→_→ 第六部分
    private class FlingRunnable implements Runnable{
        private float velocity;
        public FlingRunnable(float velocity){
            this.velocity = velocity;
        }
        public void run(){
            if ((int) Math.abs(velocity) < 20){
                isFling = false;
                return;
            }
            //減速旋轉...
            isFling = true;
            StartAngle += (velocity / 30);
            velocity /= 1.0666F;
            postDelayed(this, 30);//需要保證時刻對頁面進行刷新..因為始終要進行新的布局...
            requestLayout();
        }
    }
}

第二部分其實就是加載旁邊那個TextView和其背景的一個過程,并定義了一些相關資源...第一部分就更不用說了,一些變量的定義...看懂了三四五六部分,自然知道那些變量的作用了...最后貼一下xml文件和Activity的源代碼....

Activity....

package com.example.cil_212_app;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.Menu;
public class Web extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.web_circle);
    }
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event){
        if(keyCode==KeyEvent.KEYCODE_BACK){
            Intent intent=new Intent(Web.this,CoverActivity.class);
            startActivity(intent);
            this.finish();
        }
        return super.onKeyDown(keyCode, event);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.android, menu);
        return true;
    }
}

xml...補充一點就是我自己定義了一個資源文件...用來存放id信息使用的....這樣可以在布局中直接進行引用....

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="id_circle_menu_item_image" type="id"/>
    <item name="id_circle_menu_item_text" type="id"/>
     <item name="id_circle_menu_item_center" type="id"/>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center_horizontal"
    android:orientation="vertical" >
    <ImageView 
        android:id="@id/id_circle_menu_item_image"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"/>
    <TextView 
        android:id="@id/id_circle_menu_item_text"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:textColor="@android:color/black"
        android:textSize="14dip"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/web_bg"
    android:gravity="center_vertical"
    android:orientation="horizontal" >
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1.0"
        android:background="@drawable/turnplate_bg_left"
        android:gravity="center"
        android:orientation="vertical" >
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="Web 開發"
            android:textColor="#236B8E"
            android:textStyle="bold|italic"
            android:textSize="18dp" />
        <TextView
            android:layout_width="fill_parent"
            android:gravity="center"
            android:layout_height="wrap_content"
            android:textAlignment="textStart"
            android:layout_marginTop="5dp"
            android:textStyle="bold|italic"
            android:text="Web頁面開發,快速學會前臺頁面制作."
            android:textColor="#236B8E"
            android:textSize="13.5dip" />
    </LinearLayout>
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >
        <com.circle.cil_212_app.CircleMenuLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/turnplate_bg_right" >
            <RelativeLayout
                android:id="@id/id_circle_menu_item_center"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" >
                <ImageView
                    android:layout_width="104.0dip"
                    android:layout_height="104.0dip"
                    android:layout_centerInParent="true"
                    android:background="@drawable/turnplate_center_unlogin" />
              <!--  <ImageView
                    android:layout_width="116.0dip"
                    android:layout_height="116.0dip"
                    android:layout_centerInParent="true"
                    android:background="@drawable/turnplate_mask_unlogin_normal" />-->
            </RelativeLayout>
        </com.circle.cil_212_app.CircleMenuLayout>
    </FrameLayout>
</LinearLayout>

源碼地址:http://files.cnblogs.com/files/RGogoing/com_Draker.zip

來自: http://www.cnblogs.com/RGogoing/p/4676654.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!