皇天不负有心人,今天终于被我找到了这篇神文!关于高仿中国建设银行App的一篇Blog,于是我就不自觉的把它消化成了我的东西了,嘿嘿!不过我是有节操滴,在本文的最后我粘贴了此文转载于哪里?也希望各位在以后的学习道路上,不要做忘恩负义的人!
各位,准备好了吗?让我们一起来看看大神们是怎么玩自定义的!哈哈!来吧,上个图给大伙瞧瞧!
第一步:上来就是干!先弄弄自定义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实战 打造抽奖转盘
。 当然了,我也给大家绘制了一个图:
假设小圆是我们的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. }
画张图给大伙瞧瞧:
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.
- </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. }