Android自定义view实现圆形与半圆形菜单

作者:Jack__Frost 时间:2023-10-18 19:08:53 

前不久看到鸿洋大大的圆形菜单,就想开始模仿,因为实在是太酷了,然后自己根据别人(zw哥)给我讲的一些思路、一些分析,就开始改造自己的圆形菜单了。

文章结构:1.功能介绍以及展示;2.部分代码讲解;3.大致可以实现的UI效果展示讲解。4.源码附送。

一、功能介绍以及展示

Android自定义view实现圆形与半圆形菜单 

第一个展示是本控件的原样。但是我们可以使用很多技巧去达到我们的商业UI效果嘛。

Android自定义view实现圆形与半圆形菜单

这里给出的是本博客作品demo的展示图以及第三点的联动展示,可见是一圆型菜单,相较于鸿洋大大的那个圆形菜单多了一些需求:

1.到时候展示只需要半圆的转盘。

2.在规定的角度不能让他们自动旋转(涉及延伸的一些数学计算,一会重点讲解)。

3.要绑定fragment。

4.一个缓冲角度,即我们将要固定几个位置,而不是任意位置。我们要设计一个可能的角度去自动帮他选择。

二、代码讲解

结合实际使用的方式来讲解。分为:1.调用方式;2.此控件onMeasure方法;3.onLayout方法的作用;4.此控件事件机制dispatchTouchEvent的使用;5.数学计算—一个缓冲角度。

(1)调用方式 :(代码为展示区下方的效果代码)


//采用的是联动,使用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);
}
}

(2)此控件onMeasure方法讲解:重点讲解迭代测量


/**
* 设置布局的宽高,并策略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);

}

onMeasure深入:View在屏幕上显示出来要先经过measure(计算)和layout(布局)。这方法作用就是计算出自定义View的宽度和高度。这个计算的过程参照父布局给出的大小,以及自己特点算出结果 。当然,还有相关的尺寸测量模式。此处奉上一篇好博文:onMeasure理解。此外,我还在这方法里作为监听回调的设置!!而为控件设置图片可以直接使用我们下面设计的方法:setMenuItemIconsAndTexts一句收工。

(3)onLayout方法的讲解:(此处的圆的数学计算布置图标围绕圆位置可见鸿洋大大的推荐,讲得很清楚,当然我下面也会略微讲解下)


/**
* 设置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;
}
}

给出鸿洋大大的计算小圆的思路图:

Android自定义view实现圆形与半圆形菜单

(4)此控件事件机制dispatchTouchEvent的使用:


//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);
}

MotionEvent事件机制:(此控件我只用了三个)主要的事件类型有:ACTION_DOWN: 表示用户开始触摸。ACTION_MOVE: 表示用户在移动(手指或者其他)。ACTION_UP:表示用户抬起了手指。

(5)数学计算—一个缓冲角度。


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();
}

至于其他小的方法详情,可见源代码,有详细解释。

源码传送门:github地址:Android-自定义view之圆形与“半圆形”菜单 喜欢的可以star或fork啦,谢谢!

来源:http://blog.csdn.net/jack__frost/article/details/52965905

标签:Android,view,菜单
0
投稿

猜你喜欢

  • Spring Boot实现异步请求(Servlet 3.0)

    2023-11-27 06:26:47
  • eclipse连接不到genymotion问题的解决方案

    2022-09-05 23:26:16
  • idea手动刷新git分支的详细教程

    2022-04-05 11:53:43
  • webBrowser执行js的方法,并返回值,c#后台取值的实现

    2023-12-07 13:29:13
  • Android自定义控件实现滑动开关效果

    2022-10-25 00:45:39
  • Unity实现仿3D轮转图效果

    2023-11-24 12:26:56
  • Java Collections集合继承结构图_动力节点Java学院整理

    2022-07-10 03:44:53
  • C#客户端HttpClient请求认证及数据传输

    2023-06-11 21:11:00
  • c#装箱和拆箱知识整理

    2023-03-14 00:42:02
  • C#实现字体旋转的方法

    2023-01-19 06:41:40
  • 浅谈Async和Await如何简化异步编程(几个实例让你彻底明白)

    2021-07-28 22:41:48
  • Java获取文件的路径及常见问题解决方案

    2023-04-16 04:24:15
  • 使用adb命令向Android模拟器中导入通讯录联系人的方法

    2022-12-21 15:39:56
  • java 数据结构并查集详解

    2023-01-22 03:52:24
  • Java基础之容器Vector详解

    2023-11-25 13:10:07
  • SSH框架网上商城项目第1战之整合Struts2、Hibernate4.3和Spring4.2

    2023-04-19 15:01:55
  • Java 反射(Reflect)详解

    2022-09-27 08:34:44
  • Spring Boot整合Swagger测试api构建全纪录

    2022-10-21 09:05:25
  • java 获取当前时间的三种方法

    2022-10-11 21:54:04
  • C#面向对象设计原则之里氏替换原则

    2022-08-27 23:59:22
  • asp之家 软件编程 m.aspxhome.com