皇天不负有心人,今天终于被我找到了这篇神文!关于高仿中国建设银行App的一篇Blog,于是我就不自觉的把它消化成了我的东西了,嘿嘿!不过我是有节操滴,在本文的最后我粘贴了此文转载于哪里?也希望各位在以后的学习道路上,不要做忘恩负义的人!

各位,准备好了吗?让我们一起来看看大神们是怎么玩自定义的!哈哈!来吧,上个图给大伙瞧瞧!


android 仿建设银行圆形 模拟建设银行app_ci


android 仿建设银行圆形 模拟建设银行app_Android_02

第一步:上来就是干!先弄弄自定义View--CircleMenuLayout.java


[java]  view plain  copy




1. public class CircleMenuLayout extends ViewGroup{  
2. /**
3.      * 半径
4.      */  
5. private int mRadius;  
6. /**
7.      * 该容器内child item的默认尺寸
8.      */  
9. private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f;  
10. /**
11.      * 菜单的中心child的默认尺寸
12.      */  
13. private float RADIO_DEFAULT_CENTERITEM_DIMENSION = 1 / 3f;  
14. /**
15.      * 该容器的内边距,无视padding属性,如需边距请用该变量
16.      */  
17. private static final float RADIO_PADDING_LAYOUT = 1 / 12f;  
18. /**
19.      * 当每秒移动角度达到该值时,认为是快速移动
20.      */  
21. private static final int FLINGABLE_VALUE = 300;  
22. /**
23.      * 如果移动角度达到该值,则屏蔽点击
24.      */  
25. private static final int NOCLICK_VALUE = 3;  
26. /**
27.      * 当每秒移动角度达到该值时,认为是快速移动
28.      */  
29. private int mFlingableValue = FLINGABLE_VALUE;  
30. /**
31.      * 该容器的内边距,无视padding属性,如需边距请用该变量
32.      */  
33. private float mPadding;  
34. /**
35.      * 布局时的开始角度
36.      */  
37. private double mStartAngle = 0;  
38. /**
39.      * 菜单项的文本
40.      */  
41. private String[] mItemTexts;  
42. /**
43.      * 菜单项的图标
44.      */  
45. private int[] mItemImgs;  
46. /**
47.      * 菜单的个数
48.      */  
49. private int mMenuItemCount;  
50. /**
51.      * 检测按下到抬起时旋转的角度
52.      */  
53. private float mTmpAngle;  
54. /**
55.      * 检测按下到抬起时使用的时间
56.      */  
57. private long mDownTime;  
58. /**
59.      * 判断是否正在自动滚动
60.      */  
61. private boolean isFling;  
62. /**
63.      * 引用布局id
64.       */  
65. private int mMenuItemLayoutId = R.layout.circle_menu_item;  
66. /**
67.      * 构造函数
68.      * @param context 上下文
69.      * @param attrs 属性
70.      */  
71. public CircleMenuLayout(Context context, AttributeSet attrs) {  
72. super(context, attrs);  
73. // 无视padding  
74. 0, 0, 0, 0);  
75.     }  
76. /**
77.      * 设置布局的宽高,并策略menu item宽高
78.      */  
79. @Override  
80. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
81. int resWidth = 0;  
82. int resHeight = 0;  
83. /**
84.          * 根据传入的参数,分别获取测量模式和测量值
85.          */  
86. int width = MeasureSpec.getSize(widthMeasureSpec);  
87. int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
88.   
89. int height = MeasureSpec.getSize(heightMeasureSpec);  
90. int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
91. /**
92.          * 如果宽或者高的测量模式非精确值
93.          */  
94. if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {  
95. // 主要设置为背景图的宽度  
96.             resWidth = getSuggestedMinimumWidth();  
97. // 如果未设置背景图片,则设置为屏幕宽高的默认值  
98. 0 ? getDefaultWidth() : resWidth;  
99. // 主要设置为背景图的高度  
100.             resHeight = getSuggestedMinimumHeight();  
101. // 如果未设置背景图片,则设置为屏幕宽高的默认值  
102. 0 ? getDefaultWidth() : resHeight;  
103. else {  
104. // 如果都设置为精确值,则直接取小值;  
105.             resWidth = resHeight = Math.min(width, height);  
106.         }  
107.         setMeasuredDimension(resWidth, resHeight);  
108. // 获得半径  
109.         mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());  
110. // menu item数量  
111. final int count = getChildCount();  
112. // menu item尺寸  
113. int childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);  
114. // menu item测量模式  
115. int childMode = MeasureSpec.EXACTLY;  
116. // 迭代测量  
117. for (int i = 0; i < count; i++) {  
118. final View child = getChildAt(i);  
119. if (child.getVisibility() == GONE) {  
120. continue;  
121.             }  
122. // 计算menu item的尺寸;以及和设置好的模式,去对item进行测量  
123. int makeMeasureSpec = -1;  
124. if (child.getId() == R.id.id_circle_menu_item_center) {  
125. int) (mRadius * RADIO_DEFAULT_CENTERITEM_DIMENSION),childMode);  
126. else {  
127.                 makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);  
128.             }  
129.             child.measure(makeMeasureSpec, makeMeasureSpec);  
130.         }  
131.         mPadding = RADIO_PADDING_LAYOUT * mRadius;  
132.     }  
133. /**
134.      * MenuItem的点击事件接口
135.      * @author zhy
136.      */  
137. private OnMenuItemClickListener mOnMenuItemClickListener;  
138. public interface OnMenuItemClickListener {  
139. void itemClick(View view, int pos);  
140. void itemCenterClick(View view);  
141.     }  
142. /**
143.      * 设置MenuItem的点击事件接口
144.      * @param mOnMenuItemClickListener
145.      */  
146. public void setOnMenuItemClickListener(OnMenuItemClickListener mOnMenuItemClickListener){  
147. this.mOnMenuItemClickListener = mOnMenuItemClickListener;  
148.     }  
149. /**
150.      * 设置menu item的位置
151.      */  
152. @Override  
153. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
154. int layoutRadius = mRadius;  
155.   
156. // Laying out the child views  
157. final int childCount = getChildCount();  
158.   
159. int left, top;  
160. // menu item 的尺寸  
161. int cWidth = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);  
162.   
163. // 根据menu item的个数,计算角度  
164. float angleDelay = 360 / (getChildCount() - 1);  
165.   
166. // 遍历去设置menuitem的位置  
167. for (int i = 0; i < childCount; i++) {  
168. final View child = getChildAt(i);  
169. if (child.getId() == R.id.id_circle_menu_item_center)  
170. continue;  
171. if (child.getVisibility() == GONE) {  
172. continue;  
173.             }  
174. 360;  
175.   
176. // 计算,中心点到menu item中心的距离  
177. float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;  
178.   
179. // tmp cosa 即menu item中心点的横坐标  
180.             left = layoutRadius  
181. 2  
182. int) Math.round(tmp  
183. 1 / 2f  
184.                     * cWidth);  
185. // tmp sina 即menu item的纵坐标  
186.             top = layoutRadius  
187. 2  
188. int) Math.round(tmp  
189. 1 / 2f  
190.                     * cWidth);  
191.   
192.             child.layout(left, top, left + cWidth, top + cWidth);  
193. // 叠加尺寸  
194.             mStartAngle += angleDelay;  
195.         }  
196.   
197. // 找到中心的view,如果存在设置onclick事件  
198.         View cView = findViewById(R.id.id_circle_menu_item_center);  
199. if (cView != null) {  
200. new OnClickListener() {  
201. @Override  
202. public void onClick(View v) {  
203.   
204. if (mOnMenuItemClickListener != null) {  
205.                         mOnMenuItemClickListener.itemCenterClick(v);  
206.                     }  
207.                 }  
208.             });  
209. // 设置center item位置  
210. int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;  
211. int cr = cl + cView.getMeasuredWidth();  
212.             cView.layout(cl, cl, cr, cr);  
213.         }  
214.     }  
215. /**
216.      * 记录上一次的x,y坐标
217.      */  
218. private float mLastX;  
219. private float mLastY;  
220. /**
221.      * 自动滚动的Runnable
222.      */  
223. private AutoFlingRunnable mFlingRunnable;  
224. @Override  
225. public boolean dispatchTouchEvent(MotionEvent event) {  
226. float x = event.getX();  
227. float y = event.getY();  
228. switch (event.getAction()) {  
229. case MotionEvent.ACTION_DOWN:  
230.                 mLastX = x;  
231.                 mLastY = y;  
232.                 mDownTime = System.currentTimeMillis();  
233. 0;  
234. // 如果当前已经在快速滚动  
235. if (isFling) {  
236. // 移除快速滚动的回调  
237.                     removeCallbacks(mFlingRunnable);  
238. false;  
239. return true;  
240.                 }  
241. break;  
242. case MotionEvent.ACTION_MOVE:  
243. /**
244.                  * 获得开始的角度
245.                  */  
246. float start = getAngle(mLastX, mLastY);  
247. /**
248.                  * 获得当前的角度
249.                  */  
250. float end = getAngle(x, y);  
251. // 如果是一、四象限,则直接end-start,角度值都是正值  
252. if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {  
253.                     mStartAngle += end - start;  
254.                     mTmpAngle += end - start;  
255. else {// 二、三象限,色角度值是付值  
256.                     mStartAngle += start - end;  
257.                     mTmpAngle += start - end;  
258.                 }  
259. // 重新布局  
260.                 requestLayout();  
261.                 mLastX = x;  
262.                 mLastY = y;  
263. break;  
264. case MotionEvent.ACTION_UP:  
265. // 计算,每秒移动的角度  
266. float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);  
267. // 如果达到该值认为是快速移动  
268. if (Math.abs(anglePerSecond) > mFlingableValue && !isFling) {  
269. // post一个任务,去自动滚动  
270. new AutoFlingRunnable(anglePerSecond));  
271. return true;  
272.                 }  
273. // 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击  
274. if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {  
275. return true;  
276.                 }  
277. break;  
278.         }  
279. return super.dispatchTouchEvent(event);  
280.     }  
281. /**
282.      * 主要为了action_down时,返回true
283.      */  
284. @Override  
285. public boolean onTouchEvent(MotionEvent event) {  
286. return true;  
287.     }  
288. /**
289.      * 根据触摸的位置,计算角度
290.      * @param xTouch
291.      * @param yTouch
292.      * @return
293.      */  
294. private float getAngle(float xTouch, float yTouch) {  
295. double x = xTouch - (mRadius / 2d);  
296. double y = yTouch - (mRadius / 2d);  
297. return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);  
298.     }  
299. /**
300.      * 根据当前位置计算象限
301.      * @param x
302.      * @param y
303.      * @return
304.      */  
305. private int getQuadrant(float x, float y) {  
306. int tmpX = (int) (x - mRadius / 2);  
307. int tmpY = (int) (y - mRadius / 2);  
308. if (tmpX >= 0) {  
309. return tmpY >= 0 ? 4 : 1;  
310. else {  
311. return tmpY >= 0 ? 3 : 2;  
312.         }  
313.     }  
314. /**
315.      * 设置菜单条目的图标和文本
316.      * @param resIds
317.      */  
318. public void setMenuItemIconsAndTexts(int[] resIds, String[] texts) {  
319.         mItemImgs = resIds;  
320.         mItemTexts = texts;  
321. // 参数检查  
322. if (resIds == null && texts == null) {  
323. throw new IllegalArgumentException("菜单项文本和图片至少设置其一");  
324.         }  
325. // 初始化mMenuCount  
326. null ? texts.length : resIds.length;  
327. if (resIds != null && texts != null) {  
328.             mMenuItemCount = Math.min(resIds.length, texts.length);  
329.         }  
330.         addMenuItems();  
331.     }  
332. /**
333.      * 设置MenuItem的布局文件,必须在setMenuItemIconsAndTexts之前调用
334.      * @param mMenuItemLayoutId
335.      */  
336. public void setMenuItemLayoutId(int mMenuItemLayoutId) {  
337. this.mMenuItemLayoutId = mMenuItemLayoutId;  
338.     }  
339. /**
340.      * 添加菜单项
341.      */  
342. private void addMenuItems() {  
343.         LayoutInflater mInflater = LayoutInflater.from(getContext());  
344. /**
345.          * 根据用户设置的参数,初始化view
346.          */  
347. for (int i = 0; i < mMenuItemCount; i++) {  
348. final int j = i;  
349. this, false);  
350.             ImageView iv = (ImageView) view.findViewById(R.id.id_circle_menu_item_image);  
351.             TextView tv = (TextView) view.findViewById(R.id.id_circle_menu_item_text);  
352. if (iv != null) {  
353.                 iv.setVisibility(View.VISIBLE);  
354.                 iv.setImageResource(mItemImgs[i]);  
355. new OnClickListener() {  
356. @Override  
357. public void onClick(View v) {  
358. if (mOnMenuItemClickListener != null) {  
359.                             mOnMenuItemClickListener.itemClick(v, j);  
360.                         }  
361.                     }  
362.                 });  
363.             }  
364. if (tv != null) {  
365.                 tv.setVisibility(View.VISIBLE);  
366.                 tv.setText(mItemTexts[i]);  
367.             }  
368. // 添加view到容器中  
369.             addView(view);  
370.         }  
371.     }  
372. /**
373.      * 如果每秒旋转角度到达该值,则认为是自动滚动
374.      * @param mFlingableValue
375.      */  
376. public void setFlingableValue(int mFlingableValue) {  
377. this.mFlingableValue = mFlingableValue;  
378.     }  
379. /**
380.      * 设置内边距的比例
381.      * @param mPadding
382.      */  
383. public void setPadding(float mPadding) {  
384. this.mPadding = mPadding;  
385.     }  
386. /**
387.      * 获得默认该layout的尺寸
388.      * @return
389.      */  
390. private int getDefaultWidth() {  
391.         WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);  
392. new DisplayMetrics();  
393.         wm.getDefaultDisplay().getMetrics(outMetrics);  
394. return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);  
395.     }  
396. /**
397.      * 自动滚动的任务
398.      * @author zhy
399.      */  
400. private class AutoFlingRunnable implements Runnable {  
401. private float angelPerSecond;  
402. public AutoFlingRunnable(float velocity) {  
403. this.angelPerSecond = velocity;  
404.         }  
405. public void run() {  
406. // 如果小于20,则停止  
407. if ((int) Math.abs(angelPerSecond) < 20) {  
408. false;  
409. return;  
410.             }  
411. true;  
412. // 不断改变mStartAngle,让其滚动,/30为了避免滚动太快  
413. 30);  
414. // 逐渐减小这个值  
415. 1.0666F;  
416. this, 30);  
417. // 重新布局  
418.             requestLayout();  
419.         }  
420.     }  
421. }


简单的分析下:

[整体分析]对于上述图片的效果,我们决定自定义一个ViewGroup叫做CircleMenuLayout;

至于菜单项,文本+图片,支持设置其中任何一项,或者全部~~那么我们只需要在CircleMenuLayout的layout中去设置他们的位置就行了。
当然了在layout()之前,我们需要去进行onMeasure去测量,设置自己的宽高,和item的宽高;
最后就是和用户交互了滚动了:
我们重写dispatchTouchEvent事件,在其中编写跟随手指移动的代码~~我为什么不再onTouchEvent里面写,因为如果我在onTouchEvent里面写,用户触摸item时,我们的菜单无法移动,因为item是可点击,会作为我们的targetView,然后消耗掉我们的MOVE事件~~~关于事件分发:具体参考:Android ViewGroup事件分发机制 。 
当然了还有很多细节,如何快速滚动,什么时候应该触发item的click事件等等。
如果不清楚,请先跳过~~继续往下看~~

[部分代码分析]

1.CircleMenuLayout之onMeasure


在测量之前,我们先看看公布出去的setMenuItemIconsAndTexts,这个应该在测量之前。



[java]  view plain  copy




1. /**
2.  * 设置菜单条目的图标和文本
3.  * @param resIds
4.  */  
5. public void setMenuItemIconsAndTexts(int[] resIds, String[] texts) {  
6.     mItemImgs = resIds;  
7.     mItemTexts = texts;  
8. // 参数检查  
9. if (resIds == null && texts == null) {  
10. throw new IllegalArgumentException("菜单项文本和图片至少设置其一");  
11.     }  
12. // 初始化mMenuCount  
13. null ? texts.length : resIds.length;  
14. if (resIds != null && texts != null) {  
15.         mMenuItemCount = Math.min(resIds.length, texts.length);  
16.     }  
17.     addMenuItems();  
18. }  
19.   
20. /**
21.  * 添加菜单项
22.  */  
23. private void addMenuItems() {  
24.     LayoutInflater mInflater = LayoutInflater.from(getContext());  
25. /**
26.      * 根据用户设置的参数,初始化view
27.      */  
28. for (int i = 0; i < mMenuItemCount; i++) {  
29. final int j = i;  
30. this, false);  
31.         ImageView iv = (ImageView) view.findViewById(R.id.id_circle_menu_item_image);  
32.         TextView tv = (TextView) view.findViewById(R.id.id_circle_menu_item_text);  
33. if (iv != null) {  
34.             iv.setVisibility(View.VISIBLE);  
35.             iv.setImageResource(mItemImgs[i]);  
36. new OnClickListener() {  
37. @Override  
38. public void onClick(View v) {  
39. if (mOnMenuItemClickListener != null) {  
40.                         mOnMenuItemClickListener.itemClick(v, j);  
41.                     }  
42.                 }  
43.             });  
44.         }  
45. if (tv != null) {  
46.             tv.setVisibility(View.VISIBLE);  
47.             tv.setText(mItemTexts[i]);  
48.         }  
49. // 添加view到容器中  
50.         addView(view);  
51.     }  
52. }


比较简单,拿到两个数据,算出菜单的个数;然后去遍历,根据我们预设的R.layout.circle_menu_item,把值设上就可以。

看一眼R.layout.circle_menu_item:


[html]  view plain  copy



1. <?xml version="1.0" encoding="UTF-8"?>  
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
3. android:layout_width="wrap_content"  
4. android:layout_height="wrap_content"  
5. android:gravity="center"  
6. android:orientation="vertical" >  
7.   
8. <ImageView  
9. android:id="@id/id_circle_menu_item_image"  
10. android:layout_width="wrap_content"  
11. android:visibility="gone"  
12. android:layout_height="wrap_content" />  
13.   
14. <TextView  
15. android:id="@id/id_circle_menu_item_text"  
16. android:layout_width="wrap_content"  
17. android:visibility="gone"  
18. android:layout_height="wrap_content"  
19. android:textColor="@android:color/white"  
20. android:text="保险"  
21. android:textSize="14.0dip" />  
22.   
23. </LinearLayout>

其实就是一个布局文件,里面一个tv一个iv;注意里面的两个id,等会我就来说说它哈~~


这里大家会不会有疑问,为什么我要独立出一个布局呢,咋不在代码里面写死~~~



嗯,是这样的,我不是任性,假设我代码里面写死了,现在的需求是左边是文本右边是图标,你咋办,去改源码?我们独立出来以后呢?用户自己改改布局就行了~~~当然了,还可以把这个布局通过一个方法公布出来,setMenuItemLayoutId这样的方法。



对了上面还涉及到点击事件也就是接口的回调问题,这个so easy了,几行代码:


[java]  view plain  copy



1. /**
2.  * MenuItem的点击事件接口
3.  * @author zhy
4.  */  
5. private OnMenuItemClickListener mOnMenuItemClickListener;  
6. public interface OnMenuItemClickListener {  
7. void itemClick(View view, int pos);  
8. void itemCenterClick(View view);  
9. }  
10. /**
11.  * 设置MenuItem的点击事件接口
12.  * @param mOnMenuItemClickListener
13.  */  
14. public void setOnMenuItemClickListener(OnMenuItemClickListener mOnMenuItemClickListener){  
15. this.mOnMenuItemClickListener = mOnMenuItemClickListener;  
16. }


好了,接下来看我们声明的变量和onMeasure:


[html]  view plain  copy



1. /**  
2.  * 设置布局的宽高,并策略menu item宽高  
3.  */  
4. @Override  
5. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
6. resWidth = 0;  
7. resHeight = 0;  
8.     /**  
9.      * 根据传入的参数,分别获取测量模式和测量值  
10.      */  
11. width = MeasureSpec.getSize(widthMeasureSpec);  
12. widthMode = MeasureSpec.getMode(widthMeasureSpec);  
13.   
14. height = MeasureSpec.getSize(heightMeasureSpec);  
15. heightMode = MeasureSpec.getMode(heightMeasureSpec);  
16.     /**  
17.      * 如果宽或者高的测量模式非精确值  
18.      */  
19.     if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {  
20.         // 主要设置为背景图的宽度  
21. resWidth = getSuggestedMinimumWidth();  
22.         // 如果未设置背景图片,则设置为屏幕宽高的默认值  
23. resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;  
24.         // 主要设置为背景图的高度  
25. resHeight = getSuggestedMinimumHeight();  
26.         // 如果未设置背景图片,则设置为屏幕宽高的默认值  
27. resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;  
28.     } else {  
29.         // 如果都设置为精确值,则直接取小值;  
30. resWidth = resHeight = Math.min(width, height);  
31.     }  
32.     setMeasuredDimension(resWidth, resHeight);  
33.     // 获得半径  
34. mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());  
35.     // menu item数量  
36. count = getChildCount();  
37.     // menu item尺寸  
38. childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);  
39.     // menu item测量模式  
40. childMode = MeasureSpec.EXACTLY;  
41.     // 迭代测量  
42. i = 0; i < count; i++) {  
43. child = getChildAt(i);  
44.         if (child.getVisibility() == GONE) {  
45.             continue;  
46.         }  
47.         // 计算menu item的尺寸;以及和设置好的模式,去对item进行测量  
48. makeMeasureSpec = -1;  
49.         if (child.getId() == R.id.id_circle_menu_item_center) {  
50. makeMeasureSpec = MeasureSpec.makeMeasureSpec((int) (mRadius * RADIO_DEFAULT_CENTERITEM_DIMENSION),childMode);  
51.         } else {  
52. makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);  
53.         }  
54.         child.measure(makeMeasureSpec, makeMeasureSpec);  
55.     }  
56. mPadding = RADIO_PADDING_LAYOUT * mRadius;  
57. }


首先说一下变量:其实都有注释,mRadius是我们整个View的宽度;几个常量,分别为我们menu item的宽度占据mRadius的比例;RADIO_PADDING_LAYOUT为内边距占据的比例;剩下的自己看注释~~


测量呢?



首先我们根据widthMeasureSpec、heightMeasureSpec分别获取宽高的值和模式~~~



会不会有人会问这个值是什么玩意?怎么就能通过它拿到宽和高,没关系,恰好我们是ViewGroup,我们需要去测量子View,刚好要传这两个参数:



你往下看:child.measure(makeMeasureSpec, makeMeasureSpec);传入了两个值,你在看这两个值如何形成的,



makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,childMode);是通过MeasureSpec将尺寸和模式封装到一起的~~好了,不能再扯了,有机会独立写篇自定义控件的总结博客细说这些。





如果是EXACTLY那么简单,直接取两者的最小值即可。



如果不是,不是,那么根据设置的背景图的尺寸,如果没有背景图,那么取默认的尺寸,默认其实就是屏幕宽和高中的小值;


[java]  view plain  copy



1. /**
2.  * 获得默认该layout的尺寸
3.  * @return
4.  */  
5. private int getDefaultWidth() {  
6.     WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);  
7. new DisplayMetrics();  
8.     wm.getDefaultDisplay().getMetrics(outMetrics);  
9. return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);  
10. }


得到父控件的尺寸后,我们setMeasuredDimension设置下~~然后根据父控件的宽高,集合我们的预设常量的那些比例,去为我们的menu item设置宽和高:


没撒说的,计算出宽度,这里我们的宽度是精确值,所以我们设置menu item的模式为:EXACTLY,最后通过MeasureSpec封装,传入给child.measure(makeMeasureSpec, makeMeasureSpec);即可。



测量完成以后,那么准备布局吧~~

2.CircleMenuLayout之onLayout


我们在onLayout中将menu item设置到指定位置,理论上,我们的圆形菜单的样子就搞定了~~



[java]  view plain  copy


1. /**
2.  * 设置menu item的位置
3.  */  
4. @Override  
5. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
6. int layoutRadius = mRadius;  
7.   
8. // Laying out the child views  
9. final int childCount = getChildCount();  
10.   
11. int left, top;  
12. // menu item 的尺寸  
13. int cWidth = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);  
14.   
15. // 根据menu item的个数,计算角度  
16. float angleDelay = 360 / (getChildCount() - 1);  
17.   
18. // 遍历去设置menuitem的位置  
19. for (int i = 0; i < childCount; i++) {  
20. final View child = getChildAt(i);  
21. if (child.getId() == R.id.id_circle_menu_item_center)  
22. continue;  
23. if (child.getVisibility() == GONE) {  
24. continue;  
25.         }  
26. 360;  
27.   
28. // 计算,中心点到menu item中心的距离  
29. float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;  
30.   
31. // tmp cosa 即menu item中心点的横坐标  
32.         left = layoutRadius  
33. 2  
34. int) Math.round(tmp  
35. 1 / 2f  
36.                 * cWidth);  
37. // tmp sina 即menu item的纵坐标  
38.         top = layoutRadius  
39. 2  
40. int) Math.round(tmp  
41. 1 / 2f  
42.                 * cWidth);  
43.   
44.         child.layout(left, top, left + cWidth, top + cWidth);  
45. // 叠加尺寸  
46.         mStartAngle += angleDelay;  
47.     }  
48.   
49. // 找到中心的view,如果存在设置onclick事件  
50.     View cView = findViewById(R.id.id_circle_menu_item_center);  
51. if (cView != null) {  
52. new OnClickListener() {  
53. @Override  
54. public void onClick(View v) {  
55.   
56. if (mOnMenuItemClickListener != null) {  
57.                     mOnMenuItemClickListener.itemCenterClick(v);  
58.                 }  
59.             }  
60.         });  
61. // 设置center item位置  
62. int cl = layoutRadius / 2 - cView.getMeasuredWidth() / 2;  
63. int cr = cl + cView.getMeasuredWidth();  
64.         cView.layout(cl, cl, cr, cr);  
65.     }  
66. }

测量,无非是遍历,然后去计算left 和 top ,当然了,我们这里是圆形,所以会使用到一些数学知识。tmp*cosa 即menu item中心点的横坐标,tmp * sina 即menu item的纵坐标。关于这样的计算可以参考: Android SurfaceView实战 打造抽奖转盘

 。 当然了,我也给大家绘制了一个图:


android 仿建设银行圆形 模拟建设银行app_Math_03


假设小圆是我们的menu item,那么他的坐标就是mRadius / 2 + tmp * coas , mRadius / 2 + tmp * sina 。

如果,你只需要实现一个圆形菜单,并不需要跟随手指滚动神马的,到此就可以了,拿走不谢。

那如果还想滚动呢?请继续下文:

3.CircleMenuLayout之dispatchTouchEvent


[java]  view plain  copy


1. /**
2.  * 记录上一次的x,y坐标
3.  */  
4. private float mLastX;  
5. private float mLastY;  
6. /**
7.  * 自动滚动的Runnable
8.  */  
9. private AutoFlingRunnable mFlingRunnable;  
10.   
11. /**
12.  * 覆写父View的dispatchTouchEvent方法,事件有自己决定是否分配
13.  * @param event
14.  * @return
15.  */  
16. @Override  
17. public boolean dispatchTouchEvent(MotionEvent event) {  
18. float x = event.getX();  
19. float y = event.getY();  
20. switch (event.getAction()) {  
21. case MotionEvent.ACTION_DOWN:  
22.             mLastX = x;  
23.             mLastY = y;  
24.             mDownTime = System.currentTimeMillis();  
25. 0;  
26. // 如果当前已经在快速滚动  
27. if (isFling) {  
28. // 移除快速滚动的回调  
29.                 removeCallbacks(mFlingRunnable);  
30. false;  
31. return true;  
32.             }  
33. break;  
34. case MotionEvent.ACTION_MOVE:  
35. /**
36.              * 获得开始的角度
37.              */  
38. float start = getAngle(mLastX, mLastY);  
39. /**
40.              * 获得当前的角度
41.              */  
42. float end = getAngle(x, y);  
43. // 如果是一、四象限,则直接end-start,角度值都是正值  
44. if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {  
45.                 mStartAngle += end - start;  
46.                 mTmpAngle += end - start;  
47. else {// 二、三象限,色角度值是付值  
48.                 mStartAngle += start - end;  
49.                 mTmpAngle += start - end;  
50.             }  
51. // 重新布局  
52.             requestLayout();  
53.             mLastX = x;  
54.             mLastY = y;  
55. break;  
56. case MotionEvent.ACTION_UP:  
57. // 计算,每秒移动的角度  
58. float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);  
59. // 如果达到该值认为是快速移动  
60. if (Math.abs(anglePerSecond) > mFlingableValue && !isFling) {  
61. // post一个任务,去自动滚动  
62. new AutoFlingRunnable(anglePerSecond));  
63. return true;  
64.             }  
65. // 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击  
66. if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {  
67. return true;  
68.             }  
69. break;  
70.     }  
71. return super.dispatchTouchEvent(event);  
72. }

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:



[java]  view plain  copy




    1. /**
    2.  * 根据触摸的位置,计算角度
    3.  * @param xTouch
    4.  * @param yTouch
    5.  * @return
    6.  */  
    7. private float getAngle(float xTouch, float yTouch) {  
    8. double x = xTouch - (mRadius / 2d);  
    9. double y = yTouch - (mRadius / 2d);  
    10. return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);  
    11. }


    画张图给大伙瞧瞧:


    android 仿建设银行圆形 模拟建设银行app_Android_04


    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的值可能为负值,所以需要改变减法的顺序,因为我们最后角度需要正值;

    关于判断象限的代码:


    [java]  view plain  copy


    1. /**
    2.  * 根据当前位置计算象限
    3.  * @param x
    4.  * @param y
    5.  * @return
    6.  */  
    7. private int getQuadrant(float x, float y) {  
    8. int tmpX = (int) (x - mRadius / 2);  
    9. int tmpY = (int) (y - mRadius / 2);  
    10. if (tmpX >= 0) {  
    11. return tmpY >= 0 ? 4 : 1;  
    12. else {  
    13. return tmpY >= 0 ? 3 : 2;  
    14.     }  
    15. }


    自己拿坐标代入,立马就理解了~~~


    最后还剩什么呢?



    看我们的自动AutoFlingRunnable,自动滚动的任务~~



    我们通过post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));去触发的!


    [java]  view plain  copy




      1. /**
      2.  * 自动滚动的任务
      3.  * @author zhy
      4.  */  
      5. private class AutoFlingRunnable implements Runnable {  
      6. private float angelPerSecond;  
      7. public AutoFlingRunnable(float velocity) {  
      8. this.angelPerSecond = velocity;  
      9.     }  
      10. public void run() {  
      11. // 如果小于20,则停止  
      12. if ((int) Math.abs(angelPerSecond) < 20) {  
      13. false;  
      14. return;  
      15.         }  
      16. true;  
      17. // 不断改变mStartAngle,让其滚动,/30为了避免滚动太快  
      18. 30);  
      19. // 逐渐减小这个值  
      20. 1.0666F;  
      21. this, 30);  
      22. // 重新布局  
      23.         requestLayout();  
      24.     }  
      25. }


      代码比较短,我们传入每秒移动的角度这个值,然后根据这个值去增加mStartAngle,然后requestLayout();就是自动滚动了~~当然了需要越滚越慢和停止,所以需要


      // 逐渐减小这个值angelPerSecond /= 1.0666F; 以及最后移动很慢的时候,我们就停下来:



      // 如果小于20,则停止


      [java]  view plain  copy



      1. if ((int) Math.abs(angelPerSecond) < 20){  
      2. false;  
      3. return;  
      4. }


      到此,所有代码解析完毕~~~嘿嘿~



      那么, 接下来就和大家介绍介绍布局:

      1.activity_main.xml


      [html]  view plain  copy




      1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
      2. android:layout_width="match_parent"  
      3. android:layout_height="match_parent"  
      4. android:background="@drawable/bg"  
      5. android:gravity="center_vertical"  
      6. android:orientation="horizontal" >  
      7.   
      8. <LinearLayout  
      9. android:layout_width="0dp"  
      10. android:layout_height="wrap_content"  
      11. android:layout_weight="1.0"  
      12. android:background="@drawable/turnplate_bg_left"  
      13. android:gravity="center"  
      14. android:orientation="vertical" >  
      15.   
      16. <TextView  
      17. android:layout_width="fill_parent"  
      18. android:layout_height="wrap_content"  
      19. android:gravity="center"  
      20. android:text="手机银行"  
      21. android:textColor="#ffffff"  
      22. android:textSize="20dp" />  
      23.   
      24. <TextView  
      25. android:layout_width="fill_parent"  
      26. android:gravity="center"  
      27. android:layout_height="wrap_content"  
      28. android:layout_marginTop="5dp"  
      29. android:text="贴心的银行服务,带给您更安全便捷的智能金融体验。"  
      30. android:textColor="#ffffff"  
      31. android:textSize="13.5dip" />  
      32. </LinearLayout>  
      33.   
      34. <FrameLayout  
      35. android:layout_width="wrap_content"  
      36. android:layout_height="wrap_content" >  
      37.   
      38. <com.zanelove.CircleMenu.view.CircleMenuLayout  
      39. android:id="@+id/id_menulayout"  
      40. android:layout_width="wrap_content"  
      41. android:layout_height="wrap_content"  
      42. android:background="@drawable/turnplate_bg_right" >  
      43.   
      44. <RelativeLayout  
      45. android:id="@id/id_circle_menu_item_center"  
      46. android:layout_width="wrap_content"  
      47. android:layout_height="wrap_content" >  
      48.   
      49. <ImageView  
      50. android:layout_width="104.0dip"  
      51. android:layout_height="104.0dip"  
      52. android:layout_centerInParent="true"  
      53. android:background="@drawable/turnplate_center_unlogin" />  
      54.   
      55. <ImageView  
      56. android:layout_width="116.0dip"  
      57. android:layout_height="116.0dip"  
      58. android:layout_centerInParent="true"  
      59. android:background="@drawable/turnplate_mask_unlogin_normal" />  
      60. </RelativeLayout>  
      61. </com.zanelove.CircleMenu.view.CircleMenuLayout>  
      62. </FrameLayout>  
      63.   
      64. </LinearLayout>



      2.activity_main02.xml:


      [html]  view plain  copy


      1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
      2. android:layout_width="match_parent"  
      3. android:layout_height="match_parent"  
      4. android:background="@drawable/bg"  
      5. android:gravity="center_vertical"  
      6. android:orientation="horizontal" >  
      7.   
      8. <com.zanelove.CircleMenu.view.CircleMenuLayout  
      9. android:id="@+id/id_menulayout"  
      10. android:layout_width="match_parent"  
      11. android:layout_height="match_parent"  
      12. android:padding="100dp"  
      13. android:background="@drawable/circle_bg3" >  
      14.   
      15. <RelativeLayout  
      16. android:id="@id/id_circle_menu_item_center"  
      17. android:layout_width="wrap_content"  
      18. android:layout_height="wrap_content" >  
      19.   
      20. <ImageView  
      21. android:layout_width="104.0dip"  
      22. android:layout_height="104.0dip"  
      23. android:layout_centerInParent="true"  
      24. android:background="@drawable/turnplate_center_unlogin" />  
      25.   
      26. <ImageView  
      27. android:layout_width="116.0dip"  
      28. android:layout_height="116.0dip"  
      29. android:layout_centerInParent="true"  
      30. android:background="@drawable/turnplate_mask_unlogin_normal" />  
      31. </RelativeLayout>  
      32. </com.zanelove.CircleMenu.view.CircleMenuLayout>  
      33.
      1. </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文件:


      [html]  view plain  copy



      1. <?xml version="1.0" encoding="utf-8"?>  
      2. <resources>  
      3. <item name="id_circle_menu_item_image" type="id"/>  
      4. <item name="id_circle_menu_item_text" type="id"/>  
      5. <item name="id_circle_menu_item_center" type="id"/>  
      6. </resources>


      布局ok,为了满足上述的两张图片,我这要写两个Activity:

      1.CircleActivity.java:


      [java]  view plain  copy



      1. public class CircleActivity extends Activity {  
      2. //自定义View  
      3. private CircleMenuLayout mCircleMenuLayout;  
      4. //Item 文本  
      5. private String[] mItemTexts = new String[]{"安全中心","特色服务","投资理财","转账汇款","我的账户","信用卡"};  
      6. //Item 图片  
      7. private int[] mItemImgs = new int[]{  
      8.             R.drawable.home_mbank_1_normal,  
      9.             R.drawable.home_mbank_2_normal,  
      10.             R.drawable.home_mbank_3_normal,  
      11.             R.drawable.home_mbank_4_normal,  
      12.             R.drawable.home_mbank_5_normal,  
      13.             R.drawable.home_mbank_6_normal  
      14.     };  
      15.   
      16. @Override  
      17. protected void onCreate(Bundle savedInstanceState) {  
      18. super.onCreate(savedInstanceState);  
      19. //自己切换布局文件看效果  
      20.         setContentView(R.layout.activity_main02);  
      21.         mCircleMenuLayout = (CircleMenuLayout) findViewById(R.id.id_menulayout);  
      22.         mCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs,mItemTexts);  
      23.   
      24. new CircleMenuLayout.OnMenuItemClickListener() {  
      25. @Override  
      26. public void itemClick(View view, int pos) {  
      27. this, mItemTexts[pos], Toast.LENGTH_SHORT).show();  
      28.             }  
      29.   
      30. @Override  
      31. public void itemCenterClick(View view) {  
      32. this, "you can do something just like ccb  ", Toast.LENGTH_SHORT).show();  
      33.             }  
      34.         });  
      35.     }  
      36. }


      2.CCBActivity.java:


      [java]  view plain  copy



      1. public class CCBActivity extends Activity {  
      2. private CircleMenuLayout mCircleMenuLayout;  
      3.   
      4. private String[] mItemTexts = new String[] { "安全中心 ", "特色服务", "投资理财", "转账汇款", "我的账户", "信用卡" };  
      5. private int[] mItemImgs = new int[] {  
      6.             R.drawable.home_mbank_1_normal,  
      7.             R.drawable.home_mbank_2_normal,  
      8.             R.drawable.home_mbank_3_normal,  
      9.             R.drawable.home_mbank_4_normal,  
      10.             R.drawable.home_mbank_5_normal,  
      11.             R.drawable.home_mbank_6_normal  
      12.     };  
      13.   
      14. @Override  
      15. protected void onCreate(Bundle savedInstanceState){  
      16. super.onCreate(savedInstanceState);  
      17.   
      18. //自已切换布局文件看效果  
      19.         setContentView(R.layout.activity_main);  
      20.   
      21.         mCircleMenuLayout = (CircleMenuLayout) findViewById(R.id.id_menulayout);  
      22.         mCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs, mItemTexts);  
      23. new CircleMenuLayout.OnMenuItemClickListener() {  
      24. @Override  
      25. public void itemClick(View view, int pos) {  
      26. this, mItemTexts[pos], Toast.LENGTH_SHORT).show();  
      27.             }  
      28.   
      29. @Override  
      30. public void itemCenterClick(View view) {  
      31. this, "you can do something just like ccb  ", Toast.LENGTH_SHORT).show();  
      32.             }  
      33.         });  
      34.     }  
      35. }


      最后,各位!见证奇迹的时刻到了!
      MainActivity:


      [java]  view plain  copy


      1. public class MyActivity extends ListActivity {  
      2.   
      3. @Override  
      4. protected void onCreate(Bundle savedInstanceState) {  
      5. super.onCreate(savedInstanceState);  
      6.   
      7.         getListView().setAdapter(  
      8. new ArrayAdapter<String>(  
      9. this,  
      10.                 android.R.layout.simple_list_item_1,   
      11. new String[] {"建行圆形菜单1", "建行圆形菜单2"}  
      12.             )  
      13.         );  
      14.     }  
      15.   
      16. @Override  
      17. protected void onListItemClick(ListView l, View v, int position, long id) {  
      18. null;  
      19. if (position == 0) {  
      20. new Intent(this, CCBActivity.class);  
      21. else {  
      22. new Intent(this, CircleActivity.class);  
      23.         }  
      24.         startActivity(intent);  
      25.     }  
      26. }