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事件~~~關於事件分發:具體參考:
當然了還有很多細節,如何快速滾動,什麼時候應該觸發item的click事件等等。
如果不清楚,請先跳過~~繼續往下看~~
[部分程式碼分析]
1.CircleMenuLayout之onMeasure
在測量之前,我們先看看公佈出去的setMenuItemIconsAndTexts,這個應該在測量之前。比較簡單,拿到兩個資料,算出選單的個數;然後去遍歷,根據我們預設的R.layout.circle_menu_item,把值設上就可以。/** * 設定選單條目的圖示和文字 * @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:
<?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);
}
}