1. 程式人生 > >android開發之高仿中國建設銀行App

android開發之高仿中國建設銀行App

皇天不負有心人,今天終於被我找到了這篇神文!關於高仿中國建設銀行App的一篇Blog,於是我就不自覺的把它消化成了我的東西了,嘿嘿!不過我是有節操滴,在本文的最後我貼上了此文轉載於哪裡?也希望各位在以後的學習道路上,不要做忘恩負義的人!

各位,準備好了嗎?讓我們一起來看看大神們是怎麼玩自定義的!哈哈!來吧,上個圖給大夥瞧瞧!



第一步:上來就是幹!先弄弄自定義View--CircleMenuLayout.java

public class CircleMenuLayout extends ViewGroup{
    /**
     * 半徑
     */
    private int mRadius;
    /**
     * 該容器內child item的預設尺寸
     */
    private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f;
    /**
     * 選單的中心child的預設尺寸
     */
    private float RADIO_DEFAULT_CENTERITEM_DIMENSION = 1 / 3f;
    /**
     * 該容器的內邊距,無視padding屬性,如需邊距請用該變數
     */
    private static final float RADIO_PADDING_LAYOUT = 1 / 12f;
    /**
     * 當每秒移動角度達到該值時,認為是快速移動
     */
    private static final int FLINGABLE_VALUE = 300;
    /**
     * 如果移動角度達到該值,則遮蔽點選
     */
    private static final int NOCLICK_VALUE = 3;
    /**
     * 當每秒移動角度達到該值時,認為是快速移動
     */
    private int mFlingableValue = FLINGABLE_VALUE;
    /**
     * 該容器的內邊距,無視padding屬性,如需邊距請用該變數
     */
    private float mPadding;
    /**
     * 佈局時的開始角度
     */
    private double mStartAngle = 0;
    /**
     * 選單項的文字
     */
    private String[] mItemTexts;
    /**
     * 選單項的圖示
     */
    private int[] mItemImgs;
    /**
     * 選單的個數
     */
    private int mMenuItemCount;
    /**
     * 檢測按下到擡起時旋轉的角度
     */
    private float mTmpAngle;
    /**
     * 檢測按下到擡起時使用的時間
     */
    private long mDownTime;
    /**
     * 判斷是否正在自動滾動
     */
    private boolean isFling;
    /**
     * 引用佈局id
      */
    private int mMenuItemLayoutId = R.layout.circle_menu_item;
    /**
     * 建構函式
     * @param context 上下文
     * @param attrs 屬性
     */
    public CircleMenuLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 無視padding
        setPadding(0, 0, 0, 0);
    }
    /**
     * 設定佈局的寬高,並策略menu item寬高
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int resWidth = 0;
        int resHeight = 0;
        /**
         * 根據傳入的引數,分別獲取測量模式和測量值
         */
        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 = getSuggestedMinimumWidth();
            // 如果未設定背景圖片,則設定為螢幕寬高的預設值
            resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;
            // 主要設定為背景圖的高度
            resHeight = getSuggestedMinimumHeight();
            // 如果未設定背景圖片,則設定為螢幕寬高的預設值
            resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
        } 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 = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);
        // menu item測量模式
        int childMode = MeasureSpec.EXACTLY;
        // 迭代測量
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            // 計算menu item的尺寸;以及和設定好的模式,去對item進行測量
            int makeMeasureSpec = -1;
            if (child.getId() == R.id.id_circle_menu_item_center) {
                makeMeasureSpec = MeasureSpec.makeMeasureSpec((int) (mRadius * RADIO_DEFAULT_CENTERITEM_DIMENSION),childMode);
            } else {
                makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);
            }
            child.measure(makeMeasureSpec, makeMeasureSpec);
        }
        mPadding = RADIO_PADDING_LAYOUT * mRadius;
    }
    /**
     * MenuItem的點選事件介面
     * @author zhy
     */
    private OnMenuItemClickListener mOnMenuItemClickListener;
    public interface OnMenuItemClickListener {
        void itemClick(View view, int pos);
        void itemCenterClick(View view);
    }
    /**
     * 設定MenuItem的點選事件介面
     * @param mOnMenuItemClickListener
     */
    public void setOnMenuItemClickListener(OnMenuItemClickListener mOnMenuItemClickListener){
        this.mOnMenuItemClickListener = mOnMenuItemClickListener;
    }
    /**
     * 設定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 = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);

        // 根據menu item的個數,計算角度
        float angleDelay = 360 / (getChildCount() - 1);

        // 遍歷去設定menuitem的位置
        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;
            }
            mStartAngle %= 360;

            // 計算,中心點到menu item中心的距離
            float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;

            // tmp cosa 即menu item中心點的橫座標
            left = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                    * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                    * cWidth);
            // tmp sina 即menu item的縱座標
            top = layoutRadius
                    / 2
                    + (int) Math.round(tmp
                    * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
                    * cWidth);

            child.layout(left, top, left + cWidth, top + cWidth);
            // 疊加尺寸
            mStartAngle += angleDelay;
        }

        // 找到中心的view,如果存在設定onclick事件
        View cView = findViewById(R.id.id_circle_menu_item_center);
        if (cView != null) {
            cView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {

                    if (mOnMenuItemClickListener != null) {
                        mOnMenuItemClickListener.itemCenterClick(v);
                    }
                }
            });
            // 設定center item位置
            int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;
            int cr = cl + cView.getMeasuredWidth();
            cView.layout(cl, cl, cr, cr);
        }
    }
    /**
     * 記錄上一次的x,y座標
     */
    private float mLastX;
    private float mLastY;
    /**
     * 自動滾動的Runnable
     */
    private AutoFlingRunnable 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;
                mDownTime = System.currentTimeMillis();
                mTmpAngle = 0;
                // 如果當前已經在快速滾動
                if (isFling) {
                    // 移除快速滾動的回撥
                    removeCallbacks(mFlingRunnable);
                    isFling = false;
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * 獲得開始的角度
                 */
                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;
                }
                // 重新佈局
                requestLayout();
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                // 計算,每秒移動的角度
                float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);
                // 如果達到該值認為是快速移動
                if (Math.abs(anglePerSecond) > mFlingableValue && !isFling) {
                    // post一個任務,去自動滾動
                    post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));
                    return true;
                }
                // 如果當前旋轉角度超過NOCLICK_VALUE遮蔽點選
                if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
                    return true;
                }
                break;
        }
        return super.dispatchTouchEvent(event);
    }
    /**
     * 主要為了action_down時,返回true
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }
    /**
     * 根據觸控的位置,計算角度
     * @param xTouch
     * @param yTouch
     * @return
     */
    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);
    }
    /**
     * 根據當前位置計算象限
     * @param x
     * @param y
     * @return
     */
    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;
        }
    }
    /**
     * 設定選單條目的圖示和文字
     * @param resIds
     */
    public void setMenuItemIconsAndTexts(int[] resIds, String[] texts) {
        mItemImgs = resIds;
        mItemTexts = texts;
        // 引數檢查
        if (resIds == null && texts == null) {
            throw new IllegalArgumentException("選單項文字和圖片至少設定其一");
        }
        // 初始化mMenuCount
        mMenuItemCount = resIds == null ? texts.length : resIds.length;
        if (resIds != null && texts != null) {
            mMenuItemCount = Math.min(resIds.length, texts.length);
        }
        addMenuItems();
    }
    /**
     * 設定MenuItem的佈局檔案,必須在setMenuItemIconsAndTexts之前呼叫
     * @param mMenuItemLayoutId
     */
    public void setMenuItemLayoutId(int mMenuItemLayoutId) {
        this.mMenuItemLayoutId = mMenuItemLayoutId;
    }
    /**
     * 新增選單項
     */
    private void addMenuItems() {
        LayoutInflater mInflater = LayoutInflater.from(getContext());
        /**
         * 根據使用者設定的引數,初始化view
         */
        for (int i = 0; i < mMenuItemCount; i++) {
            final int j = i;
            View view = mInflater.inflate(mMenuItemLayoutId, 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);
            if (iv != null) {
                iv.setVisibility(View.VISIBLE);
                iv.setImageResource(mItemImgs[i]);
                iv.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnMenuItemClickListener != null) {
                            mOnMenuItemClickListener.itemClick(v, j);
                        }
                    }
                });
            }
            if (tv != null) {
                tv.setVisibility(View.VISIBLE);
                tv.setText(mItemTexts[i]);
            }
            // 新增view到容器中
            addView(view);
        }
    }
    /**
     * 如果每秒旋轉角度到達該值,則認為是自動滾動
     * @param mFlingableValue
     */
    public void setFlingableValue(int mFlingableValue) {
        this.mFlingableValue = mFlingableValue;
    }
    /**
     * 設定內邊距的比例
     * @param mPadding
     */
    public void setPadding(float mPadding) {
        this.mPadding = mPadding;
    }
    /**
     * 獲得預設該layout的尺寸
     * @return
     */
    private int getDefaultWidth() {
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
    }
    /**
     * 自動滾動的任務
     * @author zhy
     */
    private class AutoFlingRunnable implements Runnable {
        private float angelPerSecond;
        public AutoFlingRunnable(float velocity) {
            this.angelPerSecond = velocity;
        }
        public void run() {
            // 如果小於20,則停止
            if ((int) Math.abs(angelPerSecond) < 20) {
                isFling = false;
                return;
            }
            isFling = true;
            // 不斷改變mStartAngle,讓其滾動,/30為了避免滾動太快
            mStartAngle += (angelPerSecond / 30);
            // 逐漸減小這個值
            angelPerSecond /= 1.0666F;
            postDelayed(this, 30);
            // 重新佈局
            requestLayout();
        }
    }
}
簡單的分析下:

[整體分析]對於上述圖片的效果,我們決定自定義一個ViewGroup叫做CircleMenuLayout;

至於選單項,文字+圖片,支援設定其中任何一項,或者全部~~那麼我們只需要在CircleMenuLayout的layout中去設定他們的位置就行了。
當然了在layout()之前,我們需要去進行onMeasure去測量,設定自己的寬高,和item的寬高;
最後就是和使用者互動了滾動了:
我們重寫dispatchTouchEvent事件,在其中編寫跟隨手指移動的程式碼~~我為什麼不再onTouchEvent裡面寫,因為如果我在onTouchEvent裡面寫,使用者觸控item時,我們的選單無法移動,因為item是可點選,會作為我們的targetView,然後消耗掉我們的MOVE事件~~~關於事件分發:具體參考:

Android ViewGroup事件分發機制 。 
當然了還有很多細節,如何快速滾動,什麼時候應該觸發item的click事件等等。
如果不清楚,請先跳過~~繼續往下看~~

[部分程式碼分析]

1.CircleMenuLayout之onMeasure

在測量之前,我們先看看公佈出去的setMenuItemIconsAndTexts,這個應該在測量之前。
/**
 * 設定選單條目的圖示和文字
 * @param resIds
 */
public void setMenuItemIconsAndTexts(int[] resIds, String[] texts) {
	mItemImgs = resIds;
	mItemTexts = texts;
	// 引數檢查
	if (resIds == null && texts == null) {
		throw new IllegalArgumentException("選單項文字和圖片至少設定其一");
	}
	// 初始化mMenuCount
	mMenuItemCount = resIds == null ? texts.length : resIds.length;
	if (resIds != null && texts != null) {
		mMenuItemCount = Math.min(resIds.length, texts.length);
	}
	addMenuItems();
}

/**
 * 新增選單項
 */
private void addMenuItems() {
	LayoutInflater mInflater = LayoutInflater.from(getContext());
	/**
	 * 根據使用者設定的引數,初始化view
	 */
	for (int i = 0; i < mMenuItemCount; i++) {
		final int j = i;
		View view = mInflater.inflate(mMenuItemLayoutId, 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);
		if (iv != null) {
			iv.setVisibility(View.VISIBLE);
			iv.setImageResource(mItemImgs[i]);
			iv.setOnClickListener(new OnClickListener() {
				@Override
				public void onClick(View v) {
					if (mOnMenuItemClickListener != null) {
						mOnMenuItemClickListener.itemClick(v, j);
					}
				}
			});
		}
		if (tv != null) {
			tv.setVisibility(View.VISIBLE);
			tv.setText(mItemTexts[i]);
		}
		// 新增view到容器中
		addView(view);
	}
}
比較簡單,拿到兩個資料,算出選單的個數;然後去遍歷,根據我們預設的R.layout.circle_menu_item,把值設上就可以。

看一眼R.layout.circle_menu_item:

<?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"
              android:orientation="vertical" >

    <ImageView
            android:id="@id/id_circle_menu_item_image"
            android:layout_width="wrap_content"
            android:visibility="gone"
            android:layout_height="wrap_content" />

    <TextView
            android:id="@id/id_circle_menu_item_text"
            android:layout_width="wrap_content"
            android:visibility="gone"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:text="保險"
            android:textSize="14.0dip" />

</LinearLayout>
其實就是一個佈局檔案,裡面一個tv一個iv;注意裡面的兩個id,等會我就來說說它哈~~
這裡大家會不會有疑問,為什麼我要獨立出一個佈局呢,咋不在程式碼裡面寫死~~~

嗯,是這樣的,我不是任性,假設我程式碼裡面寫死了,現在的需求是左邊是文字右邊是圖示,你咋辦,去改原始碼?我們獨立出來以後呢?使用者自己改改佈局就行了~~~當然了,還可以把這個佈局通過一個方法公佈出來,setMenuItemLayoutId這樣的方法。

對了上面還涉及到點選事件也就是介面的回撥問題,這個so easy了,幾行程式碼:
/**
 * MenuItem的點選事件介面
 * @author zhy
 */
private OnMenuItemClickListener mOnMenuItemClickListener;
public interface OnMenuItemClickListener {
	void itemClick(View view, int pos);
	void itemCenterClick(View view);
}
/**
 * 設定MenuItem的點選事件介面
 * @param mOnMenuItemClickListener
 */
public void setOnMenuItemClickListener(OnMenuItemClickListener mOnMenuItemClickListener){
	this.mOnMenuItemClickListener = mOnMenuItemClickListener;
}
好了,接下來看我們宣告的變數和onMeasure:
/**
 * 設定佈局的寬高,並策略menu item寬高
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int resWidth = 0;
	int resHeight = 0;
	/**
	 * 根據傳入的引數,分別獲取測量模式和測量值
	 */
	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 = getSuggestedMinimumWidth();
		// 如果未設定背景圖片,則設定為螢幕寬高的預設值
		resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;
		// 主要設定為背景圖的高度
		resHeight = getSuggestedMinimumHeight();
		// 如果未設定背景圖片,則設定為螢幕寬高的預設值
		resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
	} 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 = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);
	// menu item測量模式
	int childMode = MeasureSpec.EXACTLY;
	// 迭代測量
	for (int i = 0; i < count; i++) {
		final View child = getChildAt(i);
		if (child.getVisibility() == GONE) {
			continue;
		}
		// 計算menu item的尺寸;以及和設定好的模式,去對item進行測量
		int makeMeasureSpec = -1;
		if (child.getId() == R.id.id_circle_menu_item_center) {
			makeMeasureSpec = MeasureSpec.makeMeasureSpec((int) (mRadius * RADIO_DEFAULT_CENTERITEM_DIMENSION),childMode);
		} else {
			makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);
		}
		child.measure(makeMeasureSpec, makeMeasureSpec);
	}
	mPadding = RADIO_PADDING_LAYOUT * mRadius;
}
首先說一下變數:其實都有註釋,mRadius是我們整個View的寬度;幾個常量,分別為我們menu item的寬度佔據mRadius的比例;RADIO_PADDING_LAYOUT為內邊距佔據的比例;剩下的自己看註釋~~
測量呢?

首先我們根據widthMeasureSpec、heightMeasureSpec分別獲取寬高的值和模式~~~

會不會有人會問這個值是什麼玩意?怎麼就能通過它拿到寬和高,沒關係,恰好我們是ViewGroup,我們需要去測量子View,剛好要傳這兩個引數:

你往下看:child.measure(makeMeasureSpec, makeMeasureSpec);傳入了兩個值,你在看這兩個值如何形成的,

makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,childMode);是通過MeasureSpec將尺寸和模式封裝到一起的~~好了,不能再扯了,有機會獨立寫篇自定義控制元件的總結部落格細說這些。

拿到尺寸和模式以後呢,我們去判斷模式,是否是EXACTLY(如果你對三種模式不瞭解,請參考:Android 手把手教您自定義ViewGroup(一)

如果是EXACTLY那麼簡單,直接取兩者的最小值即可。

如果不是,不是,那麼根據設定的背景圖的尺寸,如果沒有背景圖,那麼取預設的尺寸,預設其實就是螢幕寬和高中的小值;
/**
 * 獲得預設該layout的尺寸
 * @return
 */
private int getDefaultWidth() {
	WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
	DisplayMetrics outMetrics = new DisplayMetrics();
	wm.getDefaultDisplay().getMetrics(outMetrics);
	return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
}
得到父控制元件的尺寸後,我們setMeasuredDimension設定下~~然後根據父控制元件的寬高,集合我們的預設常量的那些比例,去為我們的menu item設定寬和高:
沒撒說的,計算出寬度,這裡我們的寬度是精確值,所以我們設定menu item的模式為:EXACTLY,最後通過MeasureSpec封裝,傳入給child.measure(makeMeasureSpec, makeMeasureSpec);即可。

測量完成以後,那麼準備佈局吧~~

2.CircleMenuLayout之onLayout

我們在onLayout中將menu item設定到指定位置,理論上,我們的圓形選單的樣子就搞定了~~
/**
 * 設定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 = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);

	// 根據menu item的個數,計算角度
	float angleDelay = 360 / (getChildCount() - 1);

	// 遍歷去設定menuitem的位置
	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;
		}
		mStartAngle %= 360;

		// 計算,中心點到menu item中心的距離
		float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;

		// tmp cosa 即menu item中心點的橫座標
		left = layoutRadius
				/ 2
				+ (int) Math.round(tmp
				* Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
				* cWidth);
		// tmp sina 即menu item的縱座標
		top = layoutRadius
				/ 2
				+ (int) Math.round(tmp
				* Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
				* cWidth);

		child.layout(left, top, left + cWidth, top + cWidth);
		// 疊加尺寸
		mStartAngle += angleDelay;
	}

	// 找到中心的view,如果存在設定onclick事件
	View cView = findViewById(R.id.id_circle_menu_item_center);
	if (cView != null) {
		cView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {

				if (mOnMenuItemClickListener != null) {
					mOnMenuItemClickListener.itemCenterClick(v);
				}
			}
		});
		// 設定center item位置
		int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;
		int cr = cl + cView.getMeasuredWidth();
		cView.layout(cl, cl, cr, cr);
	}
}
測量,無非是遍歷,然後去計算left 和 top ,當然了,我們這裡是圓形,所以會使用到一些數學知識。tmp*cosa 即menu item中心點的橫座標,tmp * sina 即menu item的縱座標。關於這樣的計算可以參考:Android SurfaceView實戰 打造抽獎轉盤 。 當然了,我也給大家繪製了一個圖:

假設小圓是我們的menu item,那麼他的座標就是mRadius / 2 + tmp * coas , mRadius / 2 + tmp * sina 。

如果,你只需要實現一個圓形選單,並不需要跟隨手指滾動神馬的,到此就可以了,拿走不謝。

那如果還想滾動呢?請繼續下文:

3.CircleMenuLayout之dispatchTouchEvent

/**
 * 記錄上一次的x,y座標
 */
private float mLastX;
private float mLastY;
/**
 * 自動滾動的Runnable
 */
private AutoFlingRunnable mFlingRunnable;

/**
 * 覆寫父View的dispatchTouchEvent方法,事件有自己決定是否分配
 * @param event
 * @return
 */
@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;
			mDownTime = System.currentTimeMillis();
			mTmpAngle = 0;
			// 如果當前已經在快速滾動
			if (isFling) {
				// 移除快速滾動的回撥
				removeCallbacks(mFlingRunnable);
				isFling = false;
				return true;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			/**
			 * 獲得開始的角度
			 */
			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;
			}
			// 重新佈局
			requestLayout();
			mLastX = x;
			mLastY = y;
			break;
		case MotionEvent.ACTION_UP:
			// 計算,每秒移動的角度
			float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);
			// 如果達到該值認為是快速移動
			if (Math.abs(anglePerSecond) > mFlingableValue && !isFling) {
				// post一個任務,去自動滾動
				post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));
				return true;
			}
			// 如果當前旋轉角度超過NOCLICK_VALUE遮蔽點選
			if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
				return true;
			}
			break;
	}
	return super.dispatchTouchEvent(event);
}
ok,程式碼並不長~~DOWN的時候,記錄下mLastX,mLastY,當前的時間,以及重置mTmpAngle為0,如果當前在自動滾動,停止該操作。
ACTION_MOVE的時候,根據mLastX,mLastY得到一個角度,再根據當前的x,y再獲得一個排程,不斷去改變mStartAngle,重新佈局介面。

當然了getAngle(x, y);方法,獲得的角度在如果是一、四象限,則直接end-start,角度值都是正值,在二、三象限,end-start角度值是負值,所以倒著減一下。

UP的時候,計算每秒移動的角度, 如果達到mFlingableValue的大小,則認為需要自動滾動,post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));去執行。

如果當前旋轉角度超過NOCLICK_VALUE遮蔽點選,遮蔽點選就是return true即可。為撒呢?因為子view的點選觸發在super.dispatchTouchEvent(event);裡面,我們直接return了。

那麼我們看上面說的一些方法,先看:getAngle:
/**
 * 根據觸控的位置,計算角度
 * @param xTouch
 * @param yTouch
 * @return
 */
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);
}

畫張圖給大夥瞧瞧:

Math.sqrt( x * x + y * y )是斜邊長,乘以 sin a 就是 y 的長度;

反之求a的角度:即Math.asin(y / Math.hypot(x, y) ; [ hypot是x * x + y * y ]

這樣我們移動的角度計算就ok了~~

不同象限,以為因為start-end的值可能為負值,所以需要改變減法的順序,因為我們最後角度需要正值;

關於判斷象限的程式碼:

/**
 * 根據當前位置計算象限
 * @param x
 * @param y
 * @return
 */
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;
	}
}
自己拿座標代入,立馬就理解了~~~
最後還剩什麼呢?

看我們的自動AutoFlingRunnable,自動滾動的任務~~

我們通過post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));去觸發的!
/**
 * 自動滾動的任務
 * @author zhy
 */
private class AutoFlingRunnable implements Runnable {
	private float angelPerSecond;
	public AutoFlingRunnable(float velocity) {
		this.angelPerSecond = velocity;
	}
	public void run() {
		// 如果小於20,則停止
		if ((int) Math.abs(angelPerSecond) < 20) {
			isFling = false;
			return;
		}
		isFling = true;
		// 不斷改變mStartAngle,讓其滾動,/30為了避免滾動太快
		mStartAngle += (angelPerSecond / 30);
		// 逐漸減小這個值
		angelPerSecond /= 1.0666F;
		postDelayed(this, 30);
		// 重新佈局
		requestLayout();
	}
}
程式碼比較短,我們傳入每秒移動的角度這個值,然後根據這個值去增加mStartAngle,然後requestLayout();就是自動滾動了~~當然了需要越滾越慢和停止,所以需要
// 逐漸減小這個值angelPerSecond /= 1.0666F; 以及最後移動很慢的時候,我們就停下來:

// 如果小於20,則停止
if ((int) Math.abs(angelPerSecond) < 20){
	isFling = false;
	return;
}

到此,所有程式碼解析完畢~~~嘿嘿~

那麼, 接下來就和大家介紹介紹佈局:

1.activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/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="手機銀行"
            android:textColor="#ffffff"
            android:textSize="20dp" />

        <TextView
            android:layout_width="fill_parent"
            android:gravity="center"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:text="貼心的銀行服務,帶給您更安全便捷的智慧金融體驗。"
            android:textColor="#ffffff"
            android:textSize="13.5dip" />
    </LinearLayout>

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >

        <com.zanelove.CircleMenu.view.CircleMenuLayout
            android:id="@+id/id_menulayout"
            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.zanelove.CircleMenu.view.CircleMenuLayout>
    </FrameLayout>

</LinearLayout>

2.activity_main02.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg"
    android:gravity="center_vertical"
    android:orientation="horizontal" >

    <com.zanelove.CircleMenu.view.CircleMenuLayout
        android:id="@+id/id_menulayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="100dp"
        android:background="@drawable/circle_bg3" >

        <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.zanelove.CircleMenu.view.CircleMenuLayout>

</LinearLayout>

佈局檔案的CircleMenuLayout中的一個控制元件為我們圓形選單的中間的那個View,當然了你可以不設定~
整個控制元件的使用,不要太簡單,一行程式碼:setMenuItemIconsAndTexts去設定文字和圖片就行~~~

如果你需要監聽click事件,通過setOnMenuItemClickListener介面即可。

ok,不知道大家有沒有注意到,我們的佈局中間的view設定的id是這樣的: android:id="@id/id_circle_menu_item_center" ,維薩呢,因為我們的自定義控制元件依賴於我們的id,所以這個id我不希望使用者自己去指定,而是提前定義些id,讓使用者去使用。其實這樣的也很常見,大家在使用一些控制元件時,某些id也需要這麼做,具體哪些控制元件,忘了~

那麼如何定義這樣的id資源呢?

在res/values下面去新建一個ids.xml檔案:

<?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>

佈局ok,為了滿足上述的兩張圖片,我這要寫兩個Activity:

1.CircleActivity.java:

public class CircleActivity extends Activity {
    //自定義View
    private CircleMenuLayout mCircleMenuLayout;
    //Item 文字
    private String[] mItemTexts = new String[]{"安全中心","特色服務","投資理財","轉賬匯款","我的賬戶","信用卡"};
    //Item 圖片
    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_6_normal
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //自己切換佈局檔案看效果
        setContentView(R.layout.activity_main02);
        mCircleMenuLayout = (CircleMenuLayout) findViewById(R.id.id_menulayout);
        mCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs,mItemTexts);

        mCircleMenuLayout.setOnMenuItemClickListener(new CircleMenuLayout.OnMenuItemClickListener() {
            @Override
            public void itemClick(View view, int pos) {
                Toast.makeText(CircleActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show();
            }

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

2.CCBActivity.java:
public class CCBActivity extends Activity {
    private CircleMenuLayout mCircleMenuLayout;

    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_6_normal
    };

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        //自已切換佈局檔案看效果
        setContentView(R.layout.activity_main);

        mCircleMenuLayout = (CircleMenuLayout) findViewById(R.id.id_menulayout);
        mCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs, mItemTexts);
        mCircleMenuLayout.setOnMenuItemClickListener(new CircleMenuLayout.OnMenuItemClickListener() {
            @Override
            public void itemClick(View view, int pos) {
                Toast.makeText(CCBActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show();
            }

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

最後,各位!見證奇蹟的時刻到了!
MainActivity:

public class MyActivity extends ListActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getListView().setAdapter(
            new ArrayAdapter<String>(
                this,
                android.R.layout.simple_list_item_1, 
                new String[] {"建行圓形選單1", "建行圓形選單2"}
            )
        );
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        Intent intent = null;
        if (position == 0) {
            intent = new Intent(this, CCBActivity.class);
        } else {
            intent = new Intent(this, CircleActivity.class);
        }
        startActivity(intent);
    }
}