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

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

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

一、功能介绍以及展示

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

这里给出的是本博客作品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;
  }
 }

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

(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啦,谢谢!

好了,Android-自定义view之圆形与“半圆形”菜单讲完了。本博客是经过仔细研究鸿洋大大的圆形菜单博客的,并在这里做出进一步拓展以及写出自己的理解。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android编程实现自定义系统菜单背景的方法

    本文实例讲述了Android编程实现自定义系统菜单背景的方法.分享给大家供大家参考,具体如下: 不多说,上图,见代码. package lab.sodino.menutest; import android.content.Context; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; import androi

  • Android自定义控件简单实现侧滑菜单效果

    侧滑菜单在很多应用中都会见到,最近QQ5.0侧滑还玩了点花样~~对于侧滑菜单,一般大家都会自定义ViewGroup,然后隐藏菜单栏,当手指滑动时,通过Scroller或者不断的改变leftMargin等实现:多少都有点复杂,完成以后还需要对滑动冲突等进行处理~~今天给大家带来一个简单的实现,史上最简单有点夸张,但是的确是我目前遇到过的最简单的一种实现~~~ 1.原理分析 既然是侧滑,无非就是在巴掌大的屏幕,塞入大概两巴掌大的布局,需要滑动可以出现另一个,既然这样,大家为啥不考虑使用Android

  • Android实现自定义滑动式抽屉效果菜单

    在Andoird使用Android自带的那些组件,像SlidingDrawer和DrawerLayout都是抽屉效果的菜单,但是在项目很多要实现的功能都收到Android这些自带组件的限制,导致很难完成项目的需求,自定义的组件,各方面都在自己的控制之下,从而根据需求做出调整.想要实现好的效果,基本上都的基于Android的OnTouch事件自己实现响应的功能. 首先,给大家先看一下整体的效果: 滑动的加速度效果都是有的,具体的体验,只能安装后才能查看. 接下来,看代码: 代码从MainActiv

  • android 自定义Android菜单背景的代码

    复制代码 代码如下: public class MenuEx extends Activity { private static final String TAG = "android123"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } @Override public b

  • Android自定义ViewGroup实现带箭头的圆角矩形菜单

    本文和大家一起做一个带箭头的圆角矩形菜单,大概长下面这个样子: 要求顶上的箭头要对准菜单锚点,菜单项按压反色,菜单背景色和按压色可配置. 最简单的做法就是让UX给个三角形的图片往上一贴,但是转念一想这样是不是太low了点,而且不同分辨率也不太好适配,干脆自定义一个ViewGroup吧! 自定义ViewGroup其实很简单,基本都是按一定的套路来的. 一.定义一个attrs.xml 就是声明一下你的这个自定义View有哪些可配置的属性,将来使用的时候可以自由配置.这里声明了7个属性,分别是:箭头宽

  • android自定义popupwindow仿微信右上角弹出菜单效果

    微信右上角的操作菜单看起来很好用,就照着仿了一下,不过是旧版微信的,手里刚好有一些旧版微信的资源图标,给大家分享一下. 不知道微信是用什么实现的,我使用popupwindow来实现,主要分为几块内容: 1.窗口布局文件:popwin_share.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com

  • Android自定义控件之仿优酷菜单

    去年的优酷HD版有过这样一种菜单,如下图: 应用打开之后,先是三个弧形的三级菜单,点击实体键menu之后,这三个菜单依次旋转退出,再点击实体键menu之后,一级菜单会旋转进入,点击一级菜单,二级菜单旋转进入,点击二级菜单的menu键,三级菜单旋转进入,再次点击二级菜单的旋转键,三级菜单又会旋转退出,这时再点击一级菜单,二级菜单退出,最后点击实体menu键,一级菜单退出. 总体来说实现这样的功能: (1)点击实体menu键时,如果界面上有菜单显示,不管有几个,全部依次退出,如果界面上没有菜单显示,

  • Android自定义VIew实现卫星菜单效果浅析

     一 概述: 最近一直致力于Android自定义VIew的学习,主要在看<android群英传>,还有CSDN博客鸿洋大神和wing大神的一些文章,写的很详细,自己心血来潮,学着写了个实现了类似卫星效果的一个自定义的View,分享到博客上,望各位指点一二.写的比较粗糙,见谅.(因为是在Linux系统下写的,效果图我直接用手机拍的,难看,大家讲究下就看个效果,勿喷). 先来看个效果图,有点不忍直视: 自定义VIew准备: (1)创建继承自View的类; (2)重写构造函数; (3)定义属性. (

  • Android实现自定义的卫星式菜单(弧形菜单)详解

    一.前言 Android 实现卫星式菜单也叫弧形菜单,主要要做的工作如下: 1.动画的处理 2.自定义ViewGroup来实现卫星式菜单View (1)自定义属性 a. 在attrs.xml中定义属性 b. 在布局中使用自定义属性 c. 在自定义View中读取布局文件中的自定义属性 (2)onMeasure 测量 child 即测量主按钮以及菜单项 (3)onLayout 布局 child 即布局主按钮以及菜单项 (4)设置主按钮的选择动画 a.为菜单项menuItem添加平移动画和旋转动画 b

  • Android编程自定义菜单实现方法详解

    本文实例讲述了Android编程自定义菜单实现方法.分享给大家供大家参考,具体如下: 在android开发的过程中系统自带的菜单往往满足不了开发中的一些需求,比如说一排最多只能放置三个菜单,坐多只能放置6个,再多的话就会折叠起来,如果我们想再一排显示4个或5个菜单那么就要自己想办法处理. 这里我用布局的隐藏并加上动画来模拟菜单的效果. 要点: 1.隐藏和显示菜单,我使用了一个线性布局把菜单封装起来. <?xml version="1.0" encoding="utf-8

  • Android使用自定义控件HorizontalScrollView打造史上最简单的侧滑菜单

    侧滑菜单在很多应用中都会见到,最近QQ5.0侧滑还玩了点花样~~对于侧滑菜单,一般大家都会自定义ViewGroup,然后隐藏菜单栏,当手指滑动时,通过Scroller或者不断的改变leftMargin等实现:多少都有点复杂,完成以后还需要对滑动冲突等进行处理~~今天给大家带来一个简单的实现,史上最简单有点夸张,但是的确是我目前遇到过的最简单的一种实现~~~ 1.原理分析 既然是侧滑,无非就是在巴掌大的屏幕,塞入大概两巴掌大的布局,需要滑动可以出现另一个,既然这样,大家为啥不考虑使用Android

  • Android开发技巧之我的菜单我做主(自定义菜单)

    Android SDK本身提供了一种默认创建菜单的机制.但通过这种机制创建的菜单虽然从功能上很完备,但在界面效果上实在是有点"土".对于一个拥有绚丽界面的程序配上一个有点"土"的菜单,会使用户感觉很怪,甚至会使绚丽的界面大打折扣.实际上,对于如此灵活和强大的Android系统,修改菜单的样式只是小菜一碟.为程序加入漂亮菜单的方法很多.在本节先介绍一种比较常用的方法,就是通过onKeyDown事件方法和PopupWindow实现自定义的菜单.至于通过这种技术能否设计出

  • Android自定义控件案例汇总2(自定义开关、下拉刷新、侧滑菜单)

    案例四 自定义开关: 功能介绍:本案例实现的功能是创建一个自定义的开关,可以自行决定开关的背景.当滑动开关时,开关的滑块可跟随手指移动.当手指松开后,滑块根据开关的状态,滑到最右边或者滑到最左边,同时保存开关的状态,将开关的状态回调给调用者.当然,上述功能系统给定的switch控件也可以实现. 实现步骤: 1. 写一个类继承view,重写两个参数的构造方法.在构造方法中指定工作空间,通过attrs.getAttributeResourceValue方法将java代码中的属性值和xml中的属性值联

随机推荐