Android 自定义组件卫星菜单的实现

发布时间 - 2026-01-11 02:09:30    点击率:

卫星菜单 ArcMenu

相信大家接触安卓,从新手到入门的过渡,就应该会了解到卫星菜单、抽屉、Xutils、Coolmenu、一些大神封装好的一些组件。这些组件在 Github 上面很容易搜得到,但是有时候打开会发现看不懂里面的代码,包括一些方法和函数 。。。。。
首先先上效果图:

实现效果

首先如果要想自定义组件

1.那么第一件事就是赋予自定义组件的属性,从效果图上看出,该组件可以存在屏幕的各个角落点,那么位置是其属性之一。

2.既然是卫星菜单,那么主按钮和其附属的小按钮之间的围绕半径也应该作为其参数之一。

3.右图得出,该组件包含很多按钮,主按钮和附属按钮,那么这个组件应该继承 ViewGroup。

一、定义卫星菜单的属性在 values 包下建立 attr 的 XML 文件,赋予组件位置属性,和半径属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <!-- 位置属性-->
  <attr name="position">
    <enum name="left_top" value="0" />
    <enum name="left_bottom" value="1" />
    <enum name="right_top" value="2" />
    <enum name="right_bottom" value="3" />
  </attr>

  <!-- 尺寸属性dp如果使用px可能会造成屏幕适配问题-->
  <attr name="radius" format="dimension" />
  <!-- 自定义属性-->
  <declare-styleable name="ArcMenu">

    <attr name="position" />
    <attr name="radius" />


  </declare-styleable>


</resources>

二、编写自定义组件

package com.lanou.dllo.arcmenudemo.arcmenu;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.RotateAnimation;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;

import com.lanou.dllo.arcmenudemo.R;

/**
 * Created by dllo on 16/3/25.
 * 1.首先ArcMenu是继承ViewGroup,那么一个卫星菜单包括一个大按钮和其他的子按钮群.
 */

public class ArcMenu extends ViewGroup implements View.OnClickListener {

  //设置常量,标识成枚举
  private static final int POS_LEFT_TOP = 0;
  private static final int POS_LEFT_BOTTOM = 1;
  private static final int POS_RIGHT_TOP = 2;
  private static final int POS_RIGHT_BOTTOM = 3;

  //以下5个成员变量是所需要的.
  //声明两个属性 位置 还有半径
  private Position mPosition = Position.RIGHT_BOTTOM;
  private int mRadius;

  /**
   * 菜单的状态
   */
  private Status mCurrentStatus = Status.CLOSE;

  /**
   * 菜单的主按钮
   */
  private View mCButton;

  //子菜单的回调按钮
  private OnMenuItemClickListener mMenuItemClickListener;


  /**
   * 菜单的位置枚举类,4个位置
   */
  public enum Position {
    LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM
  }

  public enum Status {
    OPEN, CLOSE
  }

  /**
   * 点击子菜单项,顺便把位置传递过去
   */
  public interface OnMenuItemClickListener {
    void onClick(View view, int pos);
  }

  //3个构造方法,相互传递.
  //注意别写错误.
  public ArcMenu(Context context) {
    this(context, null);
  }

  public ArcMenu(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public ArcMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //TypedValue.applyDimension是转变标准尺寸的方法 参数一:单位  参数二:默认值 参数三:可以获取当前屏幕的分辨率信息.
    mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP
        , 100, getResources().getDisplayMetrics());

    //获取自定义属性的值
    //参数1:attrs AttributeSet是节点的属性集合
    //参数2:attrs的一个数组集
    //参数3:指向当前theme 某个item 描述的style 该style指定了一些默认值为这个TypedArray
    //参数4;当defStyleAttr 找不到或者为0, 可以直接指定某个style
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
        R.styleable.ArcMenu, defStyleAttr, 0);
    int pos = a.getInt(R.styleable.ArcMenu_position, POS_RIGHT_BOTTOM);
    switch (pos) {
      case POS_LEFT_TOP:
        mPosition = Position.LEFT_TOP;
        break;
      case POS_LEFT_BOTTOM:
        mPosition = Position.LEFT_BOTTOM;
        break;
      case POS_RIGHT_TOP:
        mPosition = Position.RIGHT_TOP;
        break;
      case POS_RIGHT_BOTTOM:
        mPosition = Position.RIGHT_BOTTOM;
        break;
    }

    mRadius = (int) a.getDimension(R.styleable.ArcMenu_radius,
        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP
            , 100, getResources().getDisplayMetrics()));

    Log.d("TAG", "Position = " + mPosition + ", radius" + mRadius);
    //使用完必须回收.
    a.recycle();

  }

  public void setOnMenuItemClickListener(OnMenuItemClickListener mMenuItemClickListener) {
    this.mMenuItemClickListener = mMenuItemClickListener;
  }

  /**
   * 测量方法
   */
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
      //测量child的各个属性.
      measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (changed) {
      layoutCButton();
      //获得容器内组件的个数,并且包括这个主的组件(大按钮)
      int count = getChildCount();
      for (int i = 0; i < count - 1; i++) {
        //这里直接获取第一个,是因为getChildAt(0)是红色的按钮.
        View child = getChildAt(i + 1);
        //正常来说,如果设置按钮动画,移动出去后,是不能点击的,这里给按钮设置一个隐藏的属性.等卫星菜单飞过去,在让它们显示出来.
        child.setVisibility(View.GONE);
        /**
         * 根据画图分析,得出每个子卫星按钮的夹角 a = 90°/(菜单的个数-1)
         * 假设menu总数为4,那么从左侧数menu1的坐标为(0,R);
         * menu2的坐标为(R*sin(a),R*cos(a));
         * menu3的坐标为(R*sin(2a),R*cos(2a));
         * ...
         * menuN的坐标为(R,0);
         * 另:PI为π
         * */
        //测量每个子卫星组件的在屏幕上面的坐标距离
        //这里count-2,是因为count包含了主按钮
        //每个组件的坐标为(cl,ct);
        int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
        int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));

        int cWidth = child.getMeasuredWidth();
        int cHeight = child.getMeasuredHeight();

        //如果卫星菜单存在于底部,那么坐标位置的计算方法,就完全相反.
        /**
         * 如果菜单位置在底部 左下 ,右下.坐标会发生变化
         * */
        if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
          ct = getMeasuredHeight() - cHeight - ct;
        }

        /**
         * 右上,右下
         * */
        if (mPosition == Position.RIGHT_TOP || mPosition == Position.RIGHT_BOTTOM) {
          cl = getMeasuredWidth() - cWidth - cl;
        }


        //子布局的测量坐标;
        child.layout(cl, ct, cl + cWidth, ct + cHeight);


      }
    }

  }

  /**
   * 定位主菜单按钮
   */
  private void layoutCButton() {
    // 给主按钮设置监听
    mCButton = getChildAt(0);
    mCButton.setOnClickListener(this);

    //分别代表控件所处离左侧和上侧得距离
    int l = 0;
    int t = 0;
    int width = mCButton.getMeasuredWidth();
    int height = mCButton.getMeasuredHeight();

    /**
     * getMeasuredHeight()如果前面没有对象调用,那么这个控件继承ViewGroup,就意味着这是获取容器的总高度.
     * getMeasuredWidth()也是同理.
     * 那么就可以判断出控件在四个位置(根据坐标系判断.)
     * */
    switch (mPosition) {
      case LEFT_TOP:
        l = 0;
        t = 0;
        break;
      case LEFT_BOTTOM:
        l = 0;
        t = getMeasuredHeight() - height;
        break;
      case RIGHT_TOP:
        l = getMeasuredWidth() - width;
        t = 0;
        break;
      case RIGHT_BOTTOM:
        l = getMeasuredWidth() - width;
        t = getMeasuredHeight() - height;
        break;
    }

    //layout的四个属性.分别代表主按钮在不同位置距离屏幕左侧和上侧
    mCButton.layout(l, t, l + width, t + height);

  }


  @Override
  public void onClick(View v) {
    //主要确定mCButton的值
    mCButton = findViewById(R.id.id_button);
    if (mCButton == null) {
      mCButton = getChildAt(0);
    }

    //旋转动画
    rotateCButton(v, 0f, 360f, 300);
    //判断菜单是否关闭,如果菜单关闭需要给菜单展开,如果菜单是展开的需要给菜单关闭.
    toggleMenu(500);
  }

  /**
   * 切换菜单
   * 参数:切换菜单的时间是可控的.
   */
  public void toggleMenu(int duration) {
    //为所有子菜单添加动画. :平移动画丶旋转动画
    int count = getChildCount();
    for (int i = 0; i < count - 1; i++) {
      /**
       * 默认位置左上的话,子菜单起始坐标点为(-cl,-ct);
       *   位置右上的话,子菜单起始坐标点为(+cl,-ct);
       *   位置左下的话,子菜单起始坐标点为(-cl,+ct);
       *   位置右下的话,子菜单起始坐标点为(+cl,+ct);**
       * */
      final View childView = getChildAt(i + 1);
      //不管按钮是开还是关,子菜单必须显示才能出现动画效果.
      childView.setVisibility(View.VISIBLE);
      //平移 结束为止 0,0(以子菜单按钮当前位置,为坐标系.)
      int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
      int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));

      //创建两个判断变量,判别起始位置.
      int xflag = 1;
      int yflag = 1;
      if (mPosition == Position.LEFT_TOP
          || mPosition == Position.LEFT_BOTTOM) {
        xflag = -1;
      }
      if (mPosition == Position.LEFT_TOP
          || mPosition == Position.RIGHT_TOP) {
        yflag = -1;
      }
      //多个动画同时使用使用,用到AnimationSet
      AnimationSet animset = new AnimationSet(true);
      Animation tranAnim = null;

      //to open 打开的情况下
      if (mCurrentStatus == Status.CLOSE) {
        tranAnim = new TranslateAnimation(xflag * cl, 0, yflag * ct, 0);
        //当卫星菜单打开的时候,按钮就可以进行点击.
        childView.setClickable(true);
        childView.setFocusable(true);

      } else {//to close
        tranAnim = new TranslateAnimation(0, xflag * cl, 0, yflag * ct);
        //当卫星菜单关闭的时候,按钮也不能随之点击.
        childView.setClickable(false);
        childView.setFocusable(false);
      }
      tranAnim.setFillAfter(true);
      tranAnim.setDuration(duration);
      //设置弹出速度.
      tranAnim.setStartOffset((i * 100) / count);
      //为动画设置监听 如果需要关闭的话,在动画结束的同时,需要将子菜单的按钮全部隐藏.
      tranAnim.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {

        }

        //在动画结束时,进行设置.
        @Override
        public void onAnimationEnd(Animation animation) {
          if (mCurrentStatus == Status.CLOSE) {
//            Log.d("动画结束状态",mCurrentStatus +"");
            childView.setVisibility(View.GONE);
          }
        }

        @Override
        public void onAnimationRepeat(Animation animation) {

        }
      });

      //设置旋转动画(转两圈)
      RotateAnimation rotateAnim = new RotateAnimation(0, 720,
          Animation.RELATIVE_TO_SELF, 0.5f,
          Animation.RELATIVE_TO_SELF, 0.5f);
      rotateAnim.setDuration(duration);
      rotateAnim.setFillAfter(true);

      //把两个动画放到动画集里面
      //注意动画顺序.先增加旋转/在增加移动./
      animset.addAnimation(rotateAnim);
      animset.addAnimation(tranAnim);
      childView.startAnimation(animset);

      final int pos = i + 1;
      //设置子菜单的点击事件
      childView.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
          if (mMenuItemClickListener != null) {
            mMenuItemClickListener.onClick(childView, pos);
          }
            menuItemAnim(pos - 1);
            //切换菜单状态
            changeStatus();
        }
      });
    }
    /**
     * 当所有子菜单切换完成后,那么菜单的状态也发生了改变.
     * 所以changeStatus()必须放在循环外,
     * */
    //切换菜单状态
    changeStatus();

  }


  /**
   * 切换菜单状态
   */
  private void changeStatus() {
    //在执行一个操作之后,如果按钮是打开的在次点击就会切换状态.
    mCurrentStatus = (mCurrentStatus == Status.CLOSE ? Status.OPEN :
        Status.CLOSE);
    Log.d("动画结束状态", mCurrentStatus + "");
  }

  public boolean isOpen(){
    return mCurrentStatus ==Status.OPEN;
  }




  //设置旋转动画绕自身旋转一圈 然后持续时间为300
  private void rotateCButton(View v, float start, float end, int duration) {
    RotateAnimation anim = new RotateAnimation(start, end, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    anim.setDuration(duration);
    //保持动画旋转后的状态.
    anim.setFillAfter(true);
    v.startAnimation(anim);
  }

  /**
   * 添加menuItem的点击动画
   * */
  private void menuItemAnim(int pos) {
    for (int i = 0; i < getChildCount() - 1; i++) {
      View childView = getChildAt(i + 1);
      //在判断条件下,写入动画
      //当其中一个子菜单被点击后,自身变大并且消失
      //其他子菜单则变小消失.
      if (i == pos) {
        childView.startAnimation(scaleBigAnim(300));
      } else {
        childView.startAnimation(scaleSmallAnim(300));
      }

      //当子菜单被点击之后,其他子菜单就要变成不可被点击和获得焦点的状态,
      childView.setClickable(false);
      childView.setFocusable(false);
    }
  }

  /**
   * 为当前点击的Item设置变大和透明度降低的动画
   *
   * @param duration
   * @return
   */
  private Animation scaleBigAnim(int duration) {
    AnimationSet animationSet = new AnimationSet(true);
    ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 4.0f, 1.0f, 4.0f,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    AlphaAnimation alphaAnimation=new AlphaAnimation(1f,0.0f);

    animationSet.addAnimation(scaleAnimation);
    animationSet.addAnimation(alphaAnimation);

    animationSet.setDuration(duration);
    animationSet.setFillAfter(true);
    return animationSet;
  }

  private Animation scaleSmallAnim(int duration) {
    AnimationSet animationSet = new AnimationSet(true);
    ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    AlphaAnimation alphaAnimation=new AlphaAnimation(1f,0.0f);
    animationSet.addAnimation(scaleAnimation);
    animationSet.addAnimation(alphaAnimation);
    animationSet.setDuration(duration);
    animationSet.setFillAfter(true);
    return animationSet;
  }

}

以上就是 卫星菜单的编写,上面的注释量比较大。

这里需要注意的一点。卫星菜单在屏幕不同位置,他的动画平移值是不一样的。

如果实在不理解可以画图试试。

三、使用时注意赋予命名空间

<?xml version="1.0" encoding="utf-8"?>
<com.lanou.dllo.arcmenudemo.arcmenu.ArcMenu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:arcmenu="http://schemas.android.com/apk/res/com.lanou.dllo.arcmenudemo"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/id_menu"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  arcmenu:position="left_top"
  arcmenu:radius="140dp"
>


    <!-- 主按钮-->
    <RelativeLayout
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="@mipmap/composer_button">

      <ImageView
        android:id="@+id/id_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:src="@mipmap/composer_icn_plus" />
    </RelativeLayout>


    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_camera"
      android:tag="Camera"/>

    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_music"
      android:tag="Music"/>

    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_place"
      android:tag="Place"/>

    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_sleep"
      android:tag="Sleep"/>

    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_thought"
      android:tag="Sun"/>

    <ImageView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/composer_with"
      android:tag="People"/>

</com.lanou.dllo.arcmenudemo.arcmenu.ArcMenu>

其他的大家可以自行探索研究。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!


# Android  # 自定义组件卫星菜单  # 卫星菜单  # Android卫星菜单效果的实现方法  # Android自定义VIew实现卫星菜单效果浅析  # Android实现自定义的卫星式菜单(弧形菜单)详解  # Android编程实现仿优酷圆盘旋转菜单效果的方法详解【附demo源码下载】  # Android学习教程之圆形Menu菜单制作方法(1)  # Android自定义view实现圆形与半圆形菜单  # Android圆形旋转菜单开发实例  # Android自定义ViewGroup实现带箭头的圆角矩形菜单  # Android仿优酷圆形菜单学习笔记分享  # Adapter模式实战之重构鸿洋集团的Android圆形菜单建行  # Android实现卫星菜单效果  # 自定义  # 点为  # 是因为  # 其他的  # 就可以  # 这是  # 就会  # 放在  # 第一个  # 多个  # 找不到  # 很容易  # 希望能  # 要想  # 大神  # 弹出  # 可以直接  # 为其  # 时间为  # 其中一个 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 香港服务器租用费用高吗?如何避免常见误区?  Python文本处理实践_日志清洗解析【指导】  Android利用动画实现背景逐渐变暗  Laravel如何将应用部署到生产服务器_Laravel生产环境部署流程  Windows11怎样设置电源计划_Windows11电源计划调整攻略【指南】  原生JS获取元素集合的子元素宽度实例  Laravel如何实现一对一模型关联?(Eloquent示例)  米侠浏览器网页图片不显示怎么办 米侠图片加载修复  Java遍历集合的三种方式  如何快速建站并高效导出源代码?  Laravel怎么上传文件_Laravel图片上传及存储配置  iOS UIView常见属性方法小结  Laravel如何处理文件上传_Laravel Storage门面实现文件存储与管理  武汉网站设计制作公司,武汉有哪些比较大的同城网站或论坛,就是里面都是武汉人的?  如何在 Pandas 中基于一列条件计算另一列的分组均值  如何基于云服务器快速搭建网站及云盘系统?  奇安信“盘古石”团队突破 iOS 26.1 提权  如何用AWS免费套餐快速搭建高效网站?  Laravel如何集成第三方登录_Laravel Socialite实现微信QQ微博登录  如何登录建站主机?访问步骤全解析  长沙做网站要多少钱,长沙国安网络怎么样?  Laravel辅助函数有哪些_Laravel Helpers常用助手函数大全  哪家制作企业网站好,开办像阿里巴巴那样的网络公司和网站要怎么做?  轻松掌握MySQL函数中的last_insert_id()  阿里云高弹*务器配置方案|支持分布式架构与多节点部署  如何在局域网内绑定自建网站域名?  laravel怎么在请求结束后执行任务(Terminable Middleware)_laravel Terminable Middleware请求结束任务执行方法  公司网站制作需要多少钱,找人做公司网站需要多少钱?  Javascript中的事件循环是如何工作的_如何利用Javascript事件循环优化异步代码?  大连网站制作公司哪家好一点,大连买房网站哪个好?  Windows10电脑怎么设置虚拟光驱_Win10右键装载ISO镜像文件  Linux后台任务运行方法_nohup与&使用技巧【技巧】  Laravel如何优雅地处理服务层_在Laravel中使用Service层和Repository层  JS碰撞运动实现方法详解  猪八戒网站制作视频,开发一个猪八戒网站,大约需要多少?或者自己请程序员,需要什么程序员,多少程序员能完成?  如何在阿里云部署织梦网站?  如何快速生成高效建站系统源代码?  Laravel如何与Pusher实现实时通信?(WebSocket示例)  Laravel怎么发送邮件_Laravel Mail类SMTP配置教程  如何实现javascript表单验证_正则表达式有哪些实用技巧  python中快速进行多个字符替换的方法小结  Laravel如何使用Eloquent进行子查询  利用python获取某年中每个月的第一天和最后一天  Laravel如何创建自定义中间件?(Middleware代码示例)  JavaScript中如何操作剪贴板_ClipboardAPI怎么用  微博html5版本怎么弄发超话_超话进入入口及发帖格式要求【教程】  Laravel如何实现事件和监听器?(Event & Listener实战)  如何用IIS7快速搭建并优化网站站点?  js实现点击每个li节点,都弹出其文本值及修改  Laravel如何实现API版本控制_Laravel API版本化路由设计策略