Android-自定义view之圆形与“半圆形”菜单
OOCRaf
8年前
<p>前不久看到鸿洋大大的圆形菜单,就想开始模仿,因为实在是太酷了,然后自己根据别人(zw哥)给我讲的一些思路、一些分析,就开始改造自己的圆形菜单了。</p> <h3><strong>文章结构:1.功能介绍以及展示;2.部分代码讲解;3.大致可以实现的UI效果展示讲解。4.源码附送。</strong></h3> <h2><strong>一、功能介绍以及展示</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/211577151040d41d468f397c903f2205.gif"></p> <p style="text-align:center">这里写图片描述</p> <p>第一个展示是本控件的原样。但是我们可以使用很多技巧去达到我们的商业UI效果嘛。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b3d1fb73017f356f8615caa8067bf904.gif"></p> <p style="text-align:center">这里写图片描述</p> <p>这里给出的是本博客作品demo的展示图以及第三点的联动展示,可见是一圆型菜单,相较于鸿洋大大的那个圆形菜单多了一些需求:1.到时候展示只需要半圆的转盘。2.在规定的角度不能让他们自动旋转(涉及延伸的一些数学计算,一会重点讲解)。3.要绑定fragment。4.一个缓冲角度,即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。</p> <h2><strong>二、代码讲解:</strong></h2> <p>结合实际使用的方式来讲解。分为:1.调用方式;2.此控件onMeasure方法;3.onLayout方法的作用;4.此控件事件机制dispatchTouchEvent的使用;5.数学计算---一个缓冲角度。</p> <h3><strong>(1)调用方式 :(代码为展示区下方的效果代码)</strong></h3> <pre> <code class="language-java">//采用的是联动,使用Fragment管理器FragmentTransaction去实现fragment管理 package com.fuzhucheng.circlemenu; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.view.KeyEvent; import android.view.View; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private UpCircleMenuLayout myCircleMenuLayout; //四个fragment页面 private HomepageFragment homepageFragment; private SettingFragment settingFragment; private HistoryFragment historyFragment; private FourthFragment fourthFragment; private FifthFragment fifthFragment; private String[] mItemTexts = new String[]{"安全中心 ", "特色服务", "投资理财", "转账汇款", "我的账户", "安全中心", "特色服务", "投资理财", "转账汇款", "我的账户"}; private int[] mItemImgs = new int[]{R.drawable.home_mbank_1_normal, R.drawable.home_mbank_2_normal, R.drawable.home_mbank_3_normal, R.drawable.home_mbank_4_normal, R.drawable.home_mbank_5_normal, R.drawable.home_mbank_1_normal, R.drawable.home_mbank_2_normal, R.drawable.home_mbank_3_normal, R.drawable.home_mbank_4_normal, R.drawable.home_mbank_5_normal}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //第一次初始化首页默认显示第一个fragment initFragment1(); myCircleMenuLayout = (UpCircleMenuLayout) findViewById(R.id.id_mymenulayout); myCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs);//一句设置图片 myCircleMenuLayout.setOnMenuItemClickListener(new UpCircleMenuLayout.OnMenuItemClickListener() { @Override public void itemClick(int pos) { Toast.makeText(MainActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show(); switch (pos) { case 0: initFragment1(); setTitle("安全中心"); break; case 1: initFragment2(); setTitle("特色服务"); break; case 2: initFragment3(); setTitle("投资理财"); break; case 3: initFragment4(); setTitle("转账汇款"); break; case 4: initFragment5(); setTitle("我的账户"); break; case 5: initFragment1(); setTitle("安全中心"); break; case 6: initFragment2(); setTitle("特色服务"); break; case 7: initFragment3(); setTitle("投资理财"); break; case 8: initFragment4(); setTitle("转账汇款"); break; case 9: initFragment5(); setTitle("我的账户"); break; } } @Override public void itemCenterClick(View view) { Toast.makeText(MainActivity.this, "you can do something just like ccb ", Toast.LENGTH_SHORT).show(); } }); } //显示第一个fragment private void initFragment1(){ //开启事务,fragment的控制是由事务来实现的 homepageFragment = new HomepageFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,homepageFragment); transaction.addToBackStack(null); transaction.commit(); } //显示第二个fragment private void initFragment2(){ //开启事务,fragment的控制是由事务来实现的 settingFragment = new SettingFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,settingFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment3(){ //开启事务,fragment的控制是由事务来实现的 historyFragment = new HistoryFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,historyFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment4(){ //开启事务,fragment的控制是由事务来实现的 fourthFragment = new FourthFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,fourthFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment5(){ //开启事务,fragment的控制是由事务来实现的 fifthFragment = new FifthFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,fifthFragment); transaction.addToBackStack(null); transaction.commit(); } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { finish(); return true; } return super.onKeyDown(keyCode, event); } }</code></pre> <h3><strong>(2)此控件onMeasure方法讲解:重点讲解迭代测量</strong></h3> <pre> <code class="language-java">/** * 设置布局的宽高,并策略menu item宽高 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int resWidth = 0; int resHeight = 0; double startAngle = mStartAngle; double angle = 360 / 10; //我们传入了10个孩子 /** * 根据传入的参数,分别获取测量模式和测量值 */ int width = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); /** * 如果宽或者高的测量模式非精确值 */ if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { // 主要设置为背景图的高度 resWidth = getDefaultWidth(); resHeight = (int) (resWidth * DEFAULT_BANNER_HEIGTH / DEFAULT_BANNER_WIDTH); } else { // 如果都设置为精确值,则直接取小值; resWidth = resHeight = Math.min(width, height); } setMeasuredDimension(resWidth, resHeight); // 获得直径 mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight()); // menu item数量 final int count = getChildCount(); // menu item尺寸 int childSize; // menu item测量模式 int childMode = MeasureSpec.EXACTLY; // 迭代测量:根据孩子的数量进行遍历,为每一个孩子测量大小,设置监听回调。 for (int i = 0; i < count; i++) { final View child = getChildAt(i); startAngle = startAngle % 360; if (startAngle > 269 && startAngle < 271 && isTouchUp) { mOnMenuItemClickListener.itemClick(i); //设置监听回调。 mCurrentPosition = i; //本次使用mCurrentPosition,只是把他作为一个temp变量,可以有更多的使用,比如动态设置每个孩子相隔的角度 childSize = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);//设置大小 } else { childSize = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);//设置大小 } if (child.getVisibility() == GONE) { continue; } // 计算menu item的尺寸;以及和设置好的模式,去对item进行测量 int makeMeasureSpec = -1; makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode); child.measure(makeMeasureSpec, makeMeasureSpec); startAngle += angle; } //item容器内边距 mPadding = DensityUtil.dip2px(getContext(), RADIO_MARGIN_LAYOUT); }</code></pre> <p>onMeasure深入:View在屏幕上显示出来要先经过measure(计算)和layout(布局)。这方法作用就是计算出自定义View的宽度和高度。这个计算的过程参照父布局给出的大小,以及自己特点算出结果 。当然,还有相关的尺寸测量模式。此处奉上一篇好博文: onMeasure理解 。此外,我还在这方法里作为监听回调的设置!!而为控件设置图片可以直接使用我们下面设计的方法:setMenuItemIconsAndTexts一句收工。</p> <p>(3)onLayout方法的讲解:(此处的圆的数学计算布置图标围绕圆位置可见鸿洋大大的推荐,讲得很清楚,当然我下面也会略微讲解下)</p> <pre> <code class="language-java">/** * 设置menu item的位置 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int layoutRadius = mRadius; // Laying out the child views final int childCount = getChildCount(); int left, top; // menu item 的尺寸 int cWidth; // 根据menu item的个数,计算角度 float angleDelay = 360 / 10; // 遍历去设置menuitem的位置 for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); //根据孩子遍历,设置中间顶部那个的大小以及其他图片大小。 if (mStartAngle > 269 && mStartAngle < 271 && isTouchUp) { cWidth = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION); child.setSelected(true); } else { cWidth = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION); child.setSelected(false); } if (child.getVisibility() == GONE) { continue; } //大于360就取余归于小于360度 mStartAngle = mStartAngle % 360; float tmp = 0; //计算图片布置的中心点的圆半径。就是tmp tmp = layoutRadius / 2f - cWidth / 2 - mPadding; // tmp cosa 即menu item中心点的横坐标。计算的是item的位置,是计算位置!!! left = layoutRadius / 2 + (int) Math.round(tmp * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil .dip2px(getContext(), 1); // tmp sina 即menu item的纵坐标 top = layoutRadius / 2 + (int) Math.round(tmp * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil .dip2px(getContext(), 8); //接着当然是布置孩子的位置啦,就是根据小圆的来布置的 child.layout(left, top, left + cWidth, top + cWidth); // 叠加尺寸 mStartAngle += angleDelay; } }</code></pre> <h3>给出鸿洋大大的计算小圆的思路图:</h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/522359ed43b864f5fe32af2dd22ec886.jpg"></p> <p style="text-align:center">这里写图片描述</p> <h3><strong>(4)此控件事件机制dispatchTouchEvent的使用:</strong></h3> <pre> <code class="language-java">//dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行super.dispatchTouchEvent(ev),事件向下分发。 //onTouchEvent是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回true,表示消费了这个事件。 @Override public boolean dispatchTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //直接就是获取x,y值了,还有一个DownTime(附送) mLastX = x; mLastY = y; mDownTime = System.currentTimeMillis(); mTmpAngle = 0; break; case MotionEvent.ACTION_MOVE: isTouchUp = false; //注意isTouchUp 这个标记量!!! /** * 获得开始的角度 */ float start = getAngle(mLastX, mLastY); /** * 获得当前的角度 */ float end = getAngle(x, y); // 如果是一、四象限,则直接end-start,角度值都是正值 if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) { mStartAngle += end - start; mTmpAngle += end - start;//按下到抬起时旋转的角度 } else // 二、三象限,色角度值是负值 { mStartAngle += start - end; mTmpAngle += start - end; } // 重新布局 if (mTmpAngle != 0) { requestLayout(); } mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP: //当手指UP啦,就是关键啦,一个缓冲角度,即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。 backOrPre(); break; } return super.dispatchTouchEvent(event); }</code></pre> <p>MotionEvent事件机制:(此控件我只用了三个)主要的事件类型有:ACTION_DOWN: 表示用户开始触摸。ACTION_MOVE: 表示用户在移动(手指或者其他)。ACTION_UP:表示用户抬起了手指。</p> <h3><strong>(5)数学计算---一个缓冲角度。</strong></h3> <pre> <code class="language-java">private void backOrPre() { //缓冲的角度。即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。 isTouchUp = true; float angleDelay = 360 / 10; //这个是每个图形相隔的角度 //我们本来的上半圆的图片角度应该是:18,54,90,126,162。所以我们这里是:先让当前角度把初始的18度减去再取余每个图形相隔角度。得到的是什么呢?就是一个图片本来应该在的那堆角度。所以如果是就直接return了。 if ((mStartAngle-18)%angleDelay==0){ return; } float angle = (float)((mStartAngle-18)%36); //angle就是那个不是18度开始布局,然后是36度的整数的多出来的部分角度 //以下就是我们做的缓冲角度处理啦,如果多出来的部分角度大于图片相隔角度的一半就往前进一个,如果小于则往后退一个。 if (angleDelay/2 > angle){ mStartAngle -= angle; }else if (angleDelay/2<angle){ mStartAngle = mStartAngle - angle + angleDelay; //mStartAngle就是当前角度啦,取余36度就是多出来的角度,拿这个多出来的角度去数据处理。 } //然后重新布局onlayout requestLayout(); }</code></pre> <h3><strong>至于其他小的方法详情,可见源代码,有详细解释。</strong></h3> <p>好了,Android-自定义view之圆形与“半圆形”菜单讲完了。本博客是经过仔细研究鸿洋大大的圆形菜单博客的,并在这里做出进一步拓展以及写出自己的理解。</p> <h3> </h3> <p> </p>