Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-自定義view之圓形與“半圓形”菜單

Android-自定義view之圓形與“半圓形”菜單

編輯:關於Android編程

前不久看到鴻洋大大的圓形菜單,就想開始模仿,因為實在是太酷了,然後自己根據別人(zw哥)給我講的一些思路、一些分析,就開始改造自己的圓形菜單了。

文章結構:1.功能介紹以及展示;2.部分代碼講解;3.大致可以實現的UI效果展示講解。4.源碼附送。


一、功能介紹以及展示

這裡寫圖片描述

第一個展示是本控件的原樣。但是我們可以使用很多技巧去達到我們的商業UI效果嘛。

這裡寫圖片描述

這裡給出的是本博客作品demo的展示圖以及第三點的聯動展示,可見是一圓型菜單,相較於鴻洋大大的那個圓形菜單多了一些需求:1.到時候展示只需要半圓的轉盤。2.在規定的角度不能讓他們自動旋轉(涉及延伸的一些數學計算,一會重點講解)。3.要綁定fragment。4.一個緩沖角度,即我們將要固定幾個位置,而不是任意位置。我們要設計一個可能的角度去自動幫他選擇。


二、代碼講解:

結合實際使用的方式來講解。分為:1.調用方式;2.此控件onMeasure方法;3.onLayout方法的作用;4.此控件事件機制dispatchTouchEvent的使用;5.數學計算—一個緩沖角度。

(1)調用方式 :(代碼為展示區下方的效果代碼)

//采用的是聯動,使用Fragment管理器FragmentTransaction去實現fragment管理
package com.fuzhucheng.circlemenu;

import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private UpCircleMenuLayout myCircleMenuLayout;

    //四個fragment頁面
    private HomepageFragment homepageFragment;
    private SettingFragment settingFragment;
    private HistoryFragment historyFragment;
    private FourthFragment fourthFragment;
    private FifthFragment fifthFragment;

    private String[] mItemTexts = new String[]{"安全中心 ", "特色服務", "投資理財",
            "轉賬匯款", "我的賬戶", "安全中心", "特色服務", "投資理財", "轉賬匯款", "我的賬戶"};
    private int[] mItemImgs = 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_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};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //第一次初始化首頁默認顯示第一個fragment
        initFragment1();
        myCircleMenuLayout = (UpCircleMenuLayout) findViewById(R.id.id_mymenulayout);
        myCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs);//一句設置圖片
        myCircleMenuLayout.setOnMenuItemClickListener(new UpCircleMenuLayout.OnMenuItemClickListener() {

            @Override
            public void itemClick(int pos) {
                Toast.makeText(MainActivity.this, mItemTexts[pos],
                        Toast.LENGTH_SHORT).show();
                switch (pos) {
                    case 0:
                        initFragment1();
                        setTitle("安全中心");
                        break;
                    case 1:
                        initFragment2();
                        setTitle("特色服務");
                        break;
                    case 2:
                        initFragment3();
                        setTitle("投資理財");
                        break;
                    case 3:
                        initFragment4();
                        setTitle("轉賬匯款");
                        break;
                    case 4:
                        initFragment5();
                        setTitle("我的賬戶");
                        break;
                    case 5:
                        initFragment1();
                        setTitle("安全中心");
                        break;
                    case 6:
                        initFragment2();
                        setTitle("特色服務");
                        break;
                    case 7:
                        initFragment3();
                        setTitle("投資理財");
                        break;
                    case 8:
                        initFragment4();
                        setTitle("轉賬匯款");
                        break;
                    case 9:
                        initFragment5();
                        setTitle("我的賬戶");
                        break;
                }
            }

            @Override
            public void itemCenterClick(View view) {
                Toast.makeText(MainActivity.this,
                        "you can do something just like ccb  ",
                        Toast.LENGTH_SHORT).show();
            }
        });

    }

    //顯示第一個fragment
    private void initFragment1(){
        //開啟事務,fragment的控制是由事務來實現的

        homepageFragment = new HomepageFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fragment_tv,homepageFragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }
    //顯示第二個fragment
    private void initFragment2(){
        //開啟事務,fragment的控制是由事務來實現的

        settingFragment = new SettingFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fragment_tv,settingFragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }
    private void initFragment3(){
        //開啟事務,fragment的控制是由事務來實現的

        historyFragment = new HistoryFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fragment_tv,historyFragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }
    private void initFragment4(){
        //開啟事務,fragment的控制是由事務來實現的

        fourthFragment = new FourthFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fragment_tv,fourthFragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }
    private void initFragment5(){
        //開啟事務,fragment的控制是由事務來實現的

        fifthFragment = new FifthFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fragment_tv,fifthFragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK
                && event.getRepeatCount() == 0) {
            finish();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

(2)此控件onMeasure方法講解:重點講解迭代測量

/**
     * 設置布局的寬高,並策略menu item寬高
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int resWidth = 0;
        int resHeight = 0;
        double startAngle = mStartAngle;

        double angle = 360 / 10;   //我們傳入了10個孩子
        /**
         * 根據傳入的參數,分別獲取測量模式和測量值
         */
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        /**
         * 如果寬或者高的測量模式非精確值
         */
        if (widthMode != MeasureSpec.EXACTLY
                || heightMode != MeasureSpec.EXACTLY) {
            // 主要設置為背景圖的高度

            resWidth = getDefaultWidth();

            resHeight = (int) (resWidth * DEFAULT_BANNER_HEIGTH /
                    DEFAULT_BANNER_WIDTH);

        } else {
            // 如果都設置為精確值,則直接取小值;
            resWidth = resHeight = Math.min(width, height);
        }

        setMeasuredDimension(resWidth, resHeight);

        // 獲得直徑
        mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());

        // menu item數量
        final int count = getChildCount();
        // menu item尺寸
        int childSize;

        // menu item測量模式
        int childMode = MeasureSpec.EXACTLY;

        // 迭代測量:根據孩子的數量進行遍歷,為每一個孩子測量大小,設置監聽回調。
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            startAngle = startAngle % 360;
            if (startAngle > 269 && startAngle < 271 && isTouchUp) {
                mOnMenuItemClickListener.itemClick(i); //設置監聽回調。
                mCurrentPosition = i;  //本次使用mCurrentPosition,只是把他作為一個temp變量,可以有更多的使用,比如動態設置每個孩子相隔的角度
                childSize = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);//設置大小
            } else {
                childSize = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);//設置大小
            }
            if (child.getVisibility() == GONE) {
                continue;
            }
            // 計算menu item的尺寸;以及和設置好的模式,去對item進行測量
            int makeMeasureSpec = -1;

            makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
                    childMode);
            child.measure(makeMeasureSpec, makeMeasureSpec);
            startAngle += angle;
        }
//item容器內邊距
        mPadding = DensityUtil.dip2px(getContext(), RADIO_MARGIN_LAYOUT);

    }

onMeasure深入:View在屏幕上顯示出來要先經過measure(計算)和layout(布局)。這方法作用就是計算出自定義View的寬度和高度。這個計算的過程參照父布局給出的大小,以及自己特點算出結果 。當然,還有相關的尺寸測量模式。此處奉上一篇好博文:onMeasure理解。此外,我還在這方法裡作為監聽回調的設置!!而為控件設置圖片可以直接使用我們下面設計的方法:setMenuItemIconsAndTexts一句收工。


(3)onLayout方法的講解:(此處的圓的數學計算布置圖標圍繞圓位置可見鴻洋大大的推薦,講得很清楚,當然我下面也會略微講解下)

 /**
     * 設置menu item的位置
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int layoutRadius = mRadius;
        // Laying out the child views
        final int childCount = getChildCount();

        int left, top;
        // menu item 的尺寸
        int cWidth;

        // 根據menu item的個數,計算角度
        float angleDelay = 360 / 10;
        // 遍歷去設置menuitem的位置
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
              //根據孩子遍歷,設置中間頂部那個的大小以及其他圖片大小。
            if (mStartAngle > 269 && mStartAngle < 271 && isTouchUp) {
                cWidth = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);
                child.setSelected(true);
            } else {
                cWidth = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);
                child.setSelected(false);
            }

            if (child.getVisibility() == GONE) {
                continue;
            }
             //大於360就取余歸於小於360度
            mStartAngle = mStartAngle % 360;

            float tmp = 0;
            //計算圖片布置的中心點的圓半徑。就是tmp
            tmp = layoutRadius / 2f - cWidth / 2 - mPadding;
            // tmp cosa 即menu item中心點的橫坐標。計算的是item的位置,是計算位置!!!
            left = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                    * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                    * cWidth) + DensityUtil
                    .dip2px(getContext(), 1);
            // tmp sina 即menu item的縱坐標
            top = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                    * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil
                    .dip2px(getContext(), 8);
         //接著當然是布置孩子的位置啦,就是根據小圓的來布置的
            child.layout(left, top, left + cWidth, top + cWidth);

            // 疊加尺寸
            mStartAngle += angleDelay;
        }
    }

給出鴻洋大大的計算小圓的思路圖:

這裡寫圖片描述


(4)此控件事件機制dispatchTouchEvent的使用:

//dispatchTouchEvent是處理觸摸事件分發,事件(多數情況)是從Activity的dispatchTouchEvent開始的。執行super.dispatchTouchEvent(ev),事件向下分發。
    //onTouchEvent是View中提供的方法,ViewGroup也有這個方法,view中不提供onInterceptTouchEvent。view中默認返回true,表示消費了這個事件。
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            //直接就是獲取x,y值了,還有一個DownTime(附送)
                mLastX = x;
                mLastY = y;
                mDownTime = System.currentTimeMillis();
                mTmpAngle = 0;
                break;
            case MotionEvent.ACTION_MOVE:
                isTouchUp = false;   //注意isTouchUp 這個標記量!!!
                /**
                 * 獲得開始的角度
                 */
                float start = getAngle(mLastX, mLastY);
                /**
                 * 獲得當前的角度
                 */
                float end = getAngle(x, y);
                // 如果是一、四象限,則直接end-start,角度值都是正值
                if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {
                    mStartAngle += end - start;
                    mTmpAngle += end - start;//按下到抬起時旋轉的角度
                } else
                // 二、三象限,色角度值是負值
                {
                    mStartAngle += start - end;
                    mTmpAngle += start - end;
                }
                // 重新布局
                if (mTmpAngle != 0) {
                    requestLayout();
                }

                mLastX = x;
                mLastY = y;

                break;
            case MotionEvent.ACTION_UP:
            //當手指UP啦,就是關鍵啦,一個緩沖角度,即我們將要固定幾個位置,而不是任意位置。我們要設計一個可能的角度去自動幫他選擇。
                backOrPre();
                break;
        }
        return super.dispatchTouchEvent(event);
    }

MotionEvent事件機制:(此控件我只用了三個)主要的事件類型有:ACTION_DOWN: 表示用戶開始觸摸。ACTION_MOVE: 表示用戶在移動(手指或者其他)。ACTION_UP:表示用戶抬起了手指。


(5)數學計算—一個緩沖角度。

 private void backOrPre() {     //緩沖的角度。即我們將要固定幾個位置,而不是任意位置。我們要設計一個可能的角度去自動幫他選擇。
        isTouchUp = true;
        float angleDelay = 360 / 10;              //這個是每個圖形相隔的角度
        //我們本來的上半圓的圖片角度應該是:18,54,90,126,162。所以我們這裡是:先讓當前角度把初始的18度減去再取余每個圖形相隔角度。得到的是什麼呢?就是一個圖片本來應該在的那堆角度。所以如果是就直接return了。
        if ((mStartAngle-18)%angleDelay==0){
            return;
        }
        float angle = (float)((mStartAngle-18)%36);                 //angle就是那個不是18度開始布局,然後是36度的整數的多出來的部分角度
        //以下就是我們做的緩沖角度處理啦,如果多出來的部分角度大於圖片相隔角度的一半就往前進一個,如果小於則往後退一個。
        if (angleDelay/2 > angle){
            mStartAngle -= angle;
        }else if (angleDelay/2<angle){ mstartangle="mStartAngle" -="" angle="" pre="" data-cke-pa-onlayout="">

至於其他小的方法詳情,可見源代碼,有詳細解釋。


源碼傳送門:github地址:Android-自定義view之圓形與“半圓形”菜單 喜歡的可以star或fork啦,謝謝!

好了,Android-自定義view之圓形與“半圓形”菜單講完了。本博客是經過仔細研究鴻洋大大的圓形菜單博客的,並在這裡做出進一步拓展以及寫出自己的理解。歡迎在下面指出錯誤,共同學習!

轉載請注明:【JackFrost的博客】

更多內容,可以訪問JackFrost的博客

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved