android SectorMenuView底部导航扇形菜单的实现代码

这次分析一个扇形菜单展开的自定义View, 也是我实习期间做的一个印象比较深刻的自定义View, 前后切换了很多种实现思路, 先看看效果展示

效果展示

效果分析

  1. 点击圆形的FloatActionBar, 自身旋转一定的角度
  2. 菜单像波纹一样扩散开来
  3. 显示我们添加的item

实现分析

使用adapter适配器去设置View, 用户可自定义性强, 不过每次使用需要去设置Adapter, 较为繁琐

直接调用ItemView, 将ImageView和TextView写死, 用户操作简单, 但是缺乏可定制性(利他)

本次功能实现采用了方案 2

实现步骤

  1. 与气泡拖拽类似, 新开启一个Window进行自定义View的绘制
  2. 初始化时调用setWillNotDraw(false)方法, 强行启动ViewGroup的绘制
  3. onMeasure中将宽高写死
  4. 绘制背景
    1. 锚点为View的底部中心点
    2. 半径为屏幕宽度一半的平方和的开方(注意这里不是屏幕的一半)
  5. 添加itemView, 在onLayout中去确定其位置
  6. 添加动画效果
  7. 将相关接口暴露给外界

使用方式

BottomSectorMenuView.Converter(mFab)
        .setToggleDuration(500, 800)
        .setAnchorRotationAngle(135f)
        .addMenuItem(R.drawable.icon_camera, "拍照") { Toast.makeText(this@MainActivity, "拍照", Toast.LENGTH_SHORT).show() }
        .addMenuItem(R.drawable.icon_photo, "图片") { Toast.makeText(this@MainActivity, "图片", Toast.LENGTH_SHORT).show() }
        .addMenuItem(R.drawable.icon_text, "文字") { Toast.makeText(this@MainActivity, "文字", Toast.LENGTH_SHORT).show() }
        .addMenuItem(R.drawable.icon_video, "视频") { Toast.makeText(this@MainActivity, "视频", Toast.LENGTH_SHORT).show() }
        .addMenuItem(R.drawable.icon_camera_shooting, "摄像") { Toast.makeText(this@MainActivity, "摄像", Toast.LENGTH_SHORT).show() }
        .apply()

源码实现

/**
 * Email: frankchoochina@gmail.com
 * Created by FrankChoo on 2017/10/9.
 * Description: 底部扇形菜单, 通过Adapter添加Item
 *       1. 调用openMenu打开菜单
 *       2. 调用closeMenu关闭菜单
 */
public class SectorMenuView extends FrameLayout {
  // 每个ItemView之间的角度差
  private double mAngle;
  // 圆心坐标
  private Point mCenterPoint;
  // ItemView到圆心的半径
  private float mMaxItemRadius;
  private float mCurItemRadius;
  // 背景圆的半径
  private float mMaxBkgRadius;
  private float mCurBkgRadius;
  private Paint mPaint;

  private SectorMenuAdapter mAdapter;
  private OnMenuOpenedListener mMenuOpenedListener;
  private OnMenuClosedListener mMenuClosedListener;

  public SectorMenuView(Context context) {
    this(context, null);
  }

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

  public SectorMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    // 初始化画笔
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setDither(true);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(Color.WHITE);
    // 设置背景圆绘制的半径
    int displayWidth = getResources().getDisplayMetrics().widthPixels;
    mMaxBkgRadius = (int) Math.sqrt(Math.pow(displayWidth/2, 2.0) + Math.pow(displayWidth/2, 2.0));
    // 开启ViewGroup的绘制
    setWillNotDraw(false);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 这里直接将宽高写死, 不支持Margin
    int width = getResources().getDisplayMetrics().widthPixels;
    int height = (int) Math.sqrt(Math.pow(width / 2, 2.0) + Math.pow(width / 2, 2.0));
    setMeasuredDimension(width, height);
    // 计算半径
    int realWidth = width - getPaddingRight() - getPaddingLeft();
    int realHeight = height - getPaddingTop() - getPaddingBottom();
    mMaxItemRadius = realWidth / 2;
    // 计算圆心
    int centerX = getPaddingLeft() + realWidth / 2;
    int centerY = getPaddingTop() + realHeight;
    mCenterPoint = new Point(centerX, centerY);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    for (int i = 0; i < getChildCount(); i++) {
      View child = getChildAt(i);
      double curAngle = Math.PI - mAngle * (i + 1);
      int childCenterX = (int) (mCenterPoint.x + mCurItemRadius * Math.cos(curAngle));
      int childCenterY = (int) (mCenterPoint.y - mCurItemRadius * Math.sin(curAngle));
      child.layout(
          childCenterX - child.getMeasuredWidth() / 2,
          childCenterY - child.getMeasuredHeight() / 2,
          childCenterX + child.getMeasuredWidth() / 2,
          childCenterY + child.getMeasuredHeight() / 2
      );
      // 这里动态的去设置子View的透明度
      child.setAlpha(mCurItemRadius / mMaxItemRadius);
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCurBkgRadius, mPaint);
    super.onDraw(canvas);
  }

  public void setAdapter(SectorMenuAdapter adapter) {
    mAdapter = adapter;
    for (int i = 0; i < mAdapter.getCount(); i++) {
      View child = mAdapter.getView(i, null, this);
      addView(child);
    }
    mAngle = Math.PI / (mAdapter.getCount() + 1);
  }

  public void setBackgroudColor(@ColorInt int color) {
    mPaint.setColor(color);
  }

  public void setBackgroundResource(@ColorRes int colorResId) {
    mPaint.setColor(ContextCompat.getColor(getContext(), colorResId));
  }

  /**
   * 打开菜单
   */
  public void openMenu() {
    if (mMaxItemRadius == 0) {
      mMaxItemRadius = getResources().getDisplayMetrics().widthPixels / 2
          - getPaddingRight() - getPaddingLeft();
    }
    // 背景动画
    ValueAnimator bkgAnim = ValueAnimator.ofFloat(0f, mMaxBkgRadius).setDuration(300);
    bkgAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        mCurBkgRadius = (float) animation.getAnimatedValue();
        invalidate();
      }
    });
    // item的位置动画
    ValueAnimator itemTranslationAnim = ValueAnimator.ofFloat(0f, mMaxItemRadius).setDuration(300);
    itemTranslationAnim.setInterpolator(new OvershootInterpolator(2f));
    itemTranslationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        mCurItemRadius = (float) animation.getAnimatedValue();
        requestLayout();
      }
    });
    // 动画集合
    final AnimatorSet set = new AnimatorSet();
    set.playSequentially(bkgAnim, itemTranslationAnim);
    set.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationStart(Animator animation) {
        setAlpha(1f);
        setVisibility(View.VISIBLE);
      }
      @Override
      public void onAnimationEnd(Animator animation) {
        if (mMenuOpenedListener != null) {
          mMenuOpenedListener.opened();
        }
      }
    });
    set.start();
  }

  /**
   * 关闭菜单
   */
  public void closeMenu() {
    // Item动画
    ValueAnimator itemViewAnim = ValueAnimator.ofFloat(mMaxItemRadius, 0f).setDuration(300);
    itemViewAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        mCurItemRadius = (float) animation.getAnimatedValue();
        requestLayout();
      }
    });
    itemViewAnim.setInterpolator(new AnticipateInterpolator(2f));

    // 背景动画
    ValueAnimator backgroundAnim = ValueAnimator.ofFloat(mMaxBkgRadius, 0f).setDuration(300);
    backgroundAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        mCurBkgRadius = (float) animation.getAnimatedValue();
        invalidate();
      }
    });
    // 这里设置了该View整体透明度的变化, 防止消失的背景不在锚点处, 显示效果突兀
    ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).setDuration(250);

    // 动画集合
    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(itemViewAnim).before(backgroundAnim);
    animatorSet.play(backgroundAnim).with(alphaAnim);
    animatorSet.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
        if (mMenuClosedListener != null) {
          mMenuClosedListener.closed();
        }
        setVisibility(View.INVISIBLE);
      }
    });
    animatorSet.start();
  }

  public void setOnMenuOpenedListener(OnMenuOpenedListener listener) {
    mMenuOpenedListener = listener;
  }

  public void setOnMenuClosedListener(OnMenuClosedListener listener) {
    mMenuClosedListener = listener;
  }

  /**
   * 供外界调用的Adapter
   */
  public abstract static class SectorMenuAdapter extends BaseAdapter {

    @Override
    public long getItemId(int position) {
      return 0;
    }

    @Override
    public Object getItem(int position) {
      return null;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      return createView(position, parent);
    }

    protected abstract View createView(int position, ViewGroup parent);

    @Override
    public abstract int getCount();
  }

  public interface OnMenuOpenedListener {
    void opened();
  }

  public interface OnMenuClosedListener {
    void closed();
  }
}

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

您可能感兴趣的文章:

  • Android程序开发之Fragment实现底部导航栏实例代码
  • Android BottomNavigationBar底部导航控制器使用方法详解
  • Android实现底部导航栏功能(选项卡)
  • Android 开发之BottomBar+ViewPager+Fragment实现炫酷的底部导航效果
  • Android实现顶部底部双导航界面功能
  • Android BottomNavigationView底部导航效果
  • Android仿微信页面底部导航效果代码实现
  • Android design包自定义tablayout的底部导航栏的实现方法
  • Android中TabLayout+ViewPager 简单实现app底部Tab导航栏
(0)

相关推荐

  • Android design包自定义tablayout的底部导航栏的实现方法

    以前做项目大多用的radiobutton,今天用tablayout来做一个tab切换页面的的效果. 实现的效果就是类似QQ.微信的页面间(也就是Fragment间)的切换.如图: 布局只要一个tablayout <android.support.design.widget.TabLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id=&

  • Android中TabLayout+ViewPager 简单实现app底部Tab导航栏

    前言 在谷歌发布Android Design Support Library之前,app底部tab布局的实现方法就有很多种,其中有RadioGroup+FrameLayout.TabHost+Fragment.FragmentPagerAdapter+ViewPager等方法,虽然这些方法虽然能达到同样的效果,但我个人总觉得有些繁琐.然而,Google在2015的IO大会上,给开发者们带来了全新的Android Design Support Library,里面包含了许多新控件,这些新控件有许多

  • Android BottomNavigationView底部导航效果

    BottomNavigationView 很早之前就在 Material Design 中出现了,但是直到 Android Support Library 25 中才增加了 BottomNavigationView 控件.也就是说如果使用官方的BottomNavigationView控件必须让targetSdkVersion >= 25,这样才能引入25版本以上的兼容包. 接下来我们来看看如何使用BottomNavigationView. 使用BottomNavigationView 需要添加d

  • Android仿微信页面底部导航效果代码实现

    大家在参考本地代码的时候要根据需要适当的修改,里面有冗余代码小编没有删除.好了,废话不多说了,一切让代码说话吧! 关键代码如下所示: .java里面的主要代码 public class MainActivity extends BaseActivity implements TabChangeListener { private Fragment[] fragments; private FragZaiXianYuYue fragZaiXianYuYue; private FragDaoLuJi

  • Android程序开发之Fragment实现底部导航栏实例代码

    流行的应用的导航一般分为两种,一种是底部导航,一种是侧边栏. 说明 IDE:AS,Android studio; 模拟器:genymotion; 实现的效果,见下图. 具体实现 为了讲明白这个实现过程,我们贴出来的代码多一写,这样更方便理解 [最后还会放出完整的代码实现] .看上图的界面做的比较粗糙,但实现过程的骨架都具有了,想要更完美的设计,之后自行完善吧 ^0^. 布局 通过观察上述效果图,发现任意一个选项页面都有三部分组成: 顶部去除ActionBar后的标题栏: 中间一个Fragment

  • Android 开发之BottomBar+ViewPager+Fragment实现炫酷的底部导航效果

    BottomBar BottomBar是Github上的一个开源框架,因为从1.3.3开始不支持fragments了,要自己配置,弄了很久,不管是app的fragment还是V4 的程序总是总是闪退.于是就用这种方式实现了,效果还不错.github有详细说明,多余的就不说了. 这个roughike是这个项目的所有者(大神致敬). 我用的是Android studio开发,fragment全部导的V4的包(以为最开始就支持的是v4的,后面也支持了app.fragment). 首先是dependen

  • Android BottomNavigationBar底部导航控制器使用方法详解

    最近Google在自己推出的Material design中增加了Bottom Navigation导航控制.Android一直没有官方的导航控制器,自己实现确实是五花八门,有了这个规定之后,就类似苹果的底部Toolbar,以后我们的APP就会有一致的风格,先看一张效果: 这是官方在Material design中给出一张图,确实很不错. 1.BottomNavigationBar的下载地址 https://github.com/Ashok-Varma/BottomNavigation 2.使用

  • Android实现底部导航栏功能(选项卡)

    现在很多android的应用都采用底部导航栏的功能,这样可以使得用户在使用过程中随意切换不同的页面,现在我采用TabHost组件来自定义一个底部的导航栏的功能. 我们先看下该demo实例的框架图: 其中各个类的作用以及资源文件就不详细解释了,还有资源图片(在该Demo中借用了其它应用程序的资源图片)也不提供了,大家可以自行更换自己需要的资源图片.直接上各个布局文件或各个类的代码: 1. res/layout目录下的 maintabs.xml 源码: <?xml version="1.0&q

  • Android实现顶部底部双导航界面功能

    最近想弄一个双导航功能,查看了许多资料,总算是实现了功能,这边就算是给自己几个笔记吧! 先来看看效果 那么就开始实现了! 底部导航栏我选择用FragmentTabHost+Fragment来实现,这个方法我觉得挺好用的,代码量也不多 首先是开始的activity_main.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://s

  • android SectorMenuView底部导航扇形菜单的实现代码

    这次分析一个扇形菜单展开的自定义View, 也是我实习期间做的一个印象比较深刻的自定义View, 前后切换了很多种实现思路, 先看看效果展示 效果展示 效果分析 点击圆形的FloatActionBar, 自身旋转一定的角度 菜单像波纹一样扩散开来 显示我们添加的item 实现分析 使用adapter适配器去设置View, 用户可自定义性强, 不过每次使用需要去设置Adapter, 较为繁琐 直接调用ItemView, 将ImageView和TextView写死, 用户操作简单, 但是缺乏可定制性

  • Android实现底部导航栏功能

    本文实例为大家分享了Android实现底部导航栏功能的具体代码,供大家参考,具体内容如下 实验效果: (1)在drawable文件夹下新建tab_menu_bg.xml文件,具体代码如下: <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item

  • vue自定义底部导航栏Tabbar的实现代码

    如图所示,要完成类似的一个底部导航切换. 首先.我们需要分为5个大的VUE文件.可以根据自己的习惯来放在不同的位置. 我将5个主要的vue文件放在了5个不同的文件夹 然后,在components文件夹里新建Tabbar.vue/以及Item.vue文件 Item.vue文件如下 <template> <div class="itemWarp flex_mid" @click='changePage'> <span v-show='!bol'> <

  • Android自定义顶部导航栏控件实例代码

    下面一段代码给大家介绍了android 自定义顶部导航栏控件功能,具体代码如下所示: class HeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { //重写构造方法 在java里面 我们一般是重写三个构造方法//在kotlin中 我们可以使用

  • Android实现字母导航控件的示例代码

    目录 自定义属性 Measure测量 坐标计算 绘制 Touch事件处理 数据组装 显示效果 今天分享一个以前实现的通讯录字母导航控件,下面自定义一个类似通讯录的字母导航 View,可以知道需要自定义的几个要素,如绘制字母指示器.绘制文字.触摸监听.坐标计算等,自定义完成之后能够达到的功能如下: 完成列表数据与字母之间的相互联动; 支持布局文件属性配置; 在布局文件中能够配置相关属性,如字母颜色.字母字体大小.字母指示器颜色等属性. 主要内容如下: 自定义属性 Measure测量 坐标计算 绘制

  • Android实现底部导航栏的主界面

    在主流app中,应用的主界面都是底部含有多个标签的导航栏,点击可以切换到相应的界面,如图: 接下来将描述下其实现过程. 1.首先是分析界面,底部导航栏我们可以用一个占满屏幕宽度.包裹着数个标签TextView.方向为横向horizontal的线性布局LinearLayout.上方则是一个占满剩余空间的FrameLayout. activity_main.xml <?xml version="1.0" encoding="utf-8"?> <Line

  • android实现底部导航栏

    底部导航栏我选择用FragmentTabHost+Fragment来实现,这个方法比较好用,代码量也不多 首先是开始的activity_main.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_

随机推荐