SectorMenuView底部导航扇形菜单

作者: SharryChoo | 来源:发表于2018-02-06 18:42 被阅读75次

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

    效果展示

    扇形菜单效果展示图.gif

    效果分析

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

    实现分析

    1. 使用adapter适配器去设置View, 用户可自定义性强, 不过每次使用需要去设置Adapter, 较为繁琐
    2. 直接调用ItemView, 将ImageView和TextView写死, 用户操作简单, 但是缺乏可定制性(利他)
      本次功能实现采用了方案 2

    实现步骤

    1. 与气泡拖拽类似, 新开启一个Window进行自定义View的绘制
    2. 初始化时调用setWillNotDraw(false)方法, 强行启动ViewGroup的绘制
    3. onMeasure中将宽高写死
    4. 绘制背景
      • 锚点为View的底部中心点
      • 半径为屏幕宽度一半的平方和的开方(注意这里不是屏幕的一半)
    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()
    

    源码实现

    /**
     * Created by FrankChoo on 2017/10/18.
     * Email: frankchoochina@gmail.com
     * Version: 2.0
     * Description: 底部扇形菜单
     * 使用方式: new BottomSectorMenuView.Converter(mFab).apply()
     */
    public class BottomSectorMenuView extends ViewGroup {
    
        private final static int STATUS_MENU_OPENED = 0x000000001;
        private final static int STATUS_MENU_CLOSED = 0x000000002;
        private int mCurrentStatus = STATUS_MENU_CLOSED;
        // Duplicate 的锚定 View
        private ImageView mAnchorView;
        // 画笔
        private Paint mPaint;
        // 坐标
        private Point mCenter;
        private Point mAnchorStart;
        // 每个ItemView之间的角度差
        private double mAngle;
        // 半径
        private float mMenuItemRadius; // ItemView 到圆心的半径
        private float mBackgroundRadius;// 背景圆的半径
        // 动画执行比率
        private float mBackgroundSpreadPercent;// 当前背景动画执行百分比
        private float mMenuItemSpreadPercent;// 当前Item动画执行的百分比
        // 动画
        private AnimatorSet mMenuCloseAnimator;
        private AnimatorSet mMenuOpenAnimator;
        // 动画时长
        private long mMenuCloseDuration;
        private long mMenuOpenDuration;
        // 按钮旋转角度
        private float mAnchorRotationAngle;
        // 监听器
        private OnMenuClosedListener mListener;
    
        private BottomSectorMenuView(Context context) {
            this(context, null);
        }
    
        private BottomSectorMenuView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        private BottomSectorMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            // 初始化画笔
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setColor(Color.WHITE);
            // 初始化参数
            int width = getResources().getDisplayMetrics().widthPixels;
            int height = getResources().getDisplayMetrics().heightPixels;
            // 获取圆心坐标
            mCenter = new Point(width / 2, height);
            // 获取半径
            mMenuItemRadius = width / 2;
            mBackgroundRadius = (int) Math.sqrt(Math.pow(width / 2, 2.0) + Math.pow(width / 2, 2.0));
            setWillNotDraw(false);
        }
    
        /********************************* 以下是向外界暴露的方法*************************************/
    
        // 打开菜单
        public void openMenu() {
            if (mCurrentStatus == STATUS_MENU_OPENED) return;
            if (mMenuOpenAnimator != null && mMenuOpenAnimator.isRunning()) return;
            showMenuOpenAnimator();
        }
    
        // 关闭菜单
        public void closeMenu(OnMenuClosedListener listener) {
            if (mCurrentStatus == STATUS_MENU_CLOSED) return;
            if (mMenuCloseAnimator != null && mMenuCloseAnimator.isRunning()) return;
            showMenuCloseAnimator(listener);
        }
    
        // 菜案是否开启
        public boolean isOpen() {
            return mCurrentStatus == STATUS_MENU_OPENED;
        }
    
        public interface OnMenuClosedListener {
            void onMenuClosed();
        }
    
        /********************************* 以上是向外界暴露的方法*************************************/
    
        private void initData(View anchorView, OnMenuClosedListener listener) {
            mListener = listener;
            // 获取锚定View在屏幕内的坐标
            int[] rawPosition = new int[2];
            anchorView.getLocationOnScreen(rawPosition);
            int left = rawPosition[0];
            int top = rawPosition[1];
            mAnchorStart = new Point(left, top);
            // 制造一个与原先一样的View放在原来的位置
            mAnchorView = new ImageView(getContext());
            mAnchorView.setLayoutParams(new LayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
            mAnchorView.setImageBitmap(getBitmapFromView(anchorView));
            // 设置点击返回的效果
            mAnchorView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    closeMenu(null);
                }
            });
            addView(mAnchorView, 0);
        }
    
        private void addMenuItems(List<View> views) {
            for (View view : views) {
                addView(view);
            }
            mAngle = Math.PI / (views.size() + 1);
        }
    
        private void setToggleDuration(long open, long close) {
            mMenuOpenDuration = open;
            mMenuCloseDuration = close;
        }
    
        private void setAnchorRotationAngle(float angle) {
            mAnchorRotationAngle = angle;
        }
    
        private void showMenuOpenAnimator() {
            // 锚点动画
            ObjectAnimator anchorAnim = ObjectAnimator.ofFloat(mAnchorView, "rotation", 0f, mAnchorRotationAngle);
            anchorAnim.setInterpolator(new OvershootInterpolator(3f));
            // 背景动画
            ValueAnimator brgSpreadAnim = ValueAnimator.ofFloat(0f, 1f);
            brgSpreadAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mBackgroundSpreadPercent = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            // item的位置动画
            ValueAnimator itemSpreadAnim = ValueAnimator.ofFloat(0f, 1f);
            itemSpreadAnim.setInterpolator(new OvershootInterpolator(2f));
            itemSpreadAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mMenuItemSpreadPercent = (float) animation.getAnimatedValue();
                    requestLayout();
                }
            });
            // 动画集合
            mMenuOpenAnimator = new AnimatorSet();
            mMenuOpenAnimator.setDuration(mMenuOpenDuration == 0 ? 800 : mMenuOpenDuration);
            mMenuOpenAnimator.play(brgSpreadAnim).before(itemSpreadAnim);
            mMenuOpenAnimator.play(anchorAnim).with(brgSpreadAnim);
            mMenuOpenAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    setAlpha(1f);
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    mCurrentStatus = STATUS_MENU_OPENED;
                }
            });
            mMenuOpenAnimator.start();
        }
    
        private void showMenuCloseAnimator(final OnMenuClosedListener listener) {
            // Item动画
            ValueAnimator itemViewAnim = ValueAnimator.ofFloat(1f, 0f)
                    .setDuration(mMenuCloseDuration == 0 ? 300 : mMenuCloseDuration / 2);
            itemViewAnim.setInterpolator(new AnticipateInterpolator(2f));
            itemViewAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mMenuItemSpreadPercent = (float) animation.getAnimatedValue();
                    requestLayout();
                }
            });
    
            // 背景动画
            ValueAnimator backgroundAnim = ValueAnimator.ofFloat(1f, 0f)
                    .setDuration(mMenuCloseDuration == 0 ? 300 : mMenuCloseDuration / 2);
            backgroundAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mBackgroundSpreadPercent = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            // 这里设置了该View整体透明度的变化, 防止消失的背景不在锚点处, 显示效果突兀
            ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
                    .setDuration(mMenuCloseDuration == 0 ? 300 : mMenuCloseDuration / 2);
    
            // 锚点动画
            ObjectAnimator anchorAnim = ObjectAnimator.ofFloat(mAnchorView, "rotation", mAnchorRotationAngle, 0f)
                    .setDuration(mMenuCloseDuration == 0 ? 300 : mMenuCloseDuration / 2);
            anchorAnim.setInterpolator(new OvershootInterpolator(3f));
    
            // 动画集合
            mMenuCloseAnimator = new AnimatorSet();
            mMenuCloseAnimator.play(itemViewAnim).before(backgroundAnim);
            mMenuCloseAnimator.play(alphaAnim).with(backgroundAnim);
            mMenuCloseAnimator.play(anchorAnim).with(backgroundAnim);
            mMenuCloseAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mCurrentStatus = STATUS_MENU_CLOSED;
                    // Convert回调
                    mListener.onMenuClosed();
                    // 用户回调
                    if (listener != null) {
                        listener.onMenuClosed();
                    }
                }
            });
            mMenuCloseAnimator.start();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (i == 0) {
                    child.layout(
                            mAnchorStart.x,
                            mAnchorStart.y,
                            mAnchorStart.x + child.getMeasuredWidth(),
                            mAnchorStart.y + child.getMeasuredHeight()
                    );
                    continue;
                }
                // 动态的计算角度
                double curAngle = Math.PI - mAngle * i;
                int childCenterX = (int) (mCenter.x + mMenuItemRadius * mMenuItemSpreadPercent * Math.cos(curAngle));
                int childCenterY = (int) (mCenter.y - mMenuItemRadius * mMenuItemSpreadPercent * Math.sin(curAngle));
                child.layout(
                        childCenterX - child.getMeasuredWidth() / 2,
                        childCenterY - child.getMeasuredHeight() / 2,
                        childCenterX + child.getMeasuredWidth() / 2,
                        childCenterY + child.getMeasuredHeight() / 2
                );
                // 动态的去设置ItemView的透明度
                child.setAlpha(mMenuItemSpreadPercent);
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            if (mAnchorView == null)
                return;
            if (mBackgroundSpreadPercent > 0) {
                canvas.drawCircle(mCenter.x, mCenter.y, mBackgroundRadius * mBackgroundSpreadPercent, mPaint);
            }
            super.onDraw(canvas);
        }
    
        private Bitmap getBitmapFromView(View view) {
            // 创建缓存
            view.buildDrawingCache();
            // 获取缓存
            return view.getDrawingCache();
        }
    
        /**
         * 外界用于构建菜单的类
         */
        public static class Converter implements OnMenuClosedListener {
    
            private Context mContext;
            private View mAnchorView;
            private WindowManager mWindowManager;
            private ContainerView mWindowContainer;
            private BottomSectorMenuView mBottomMenuView;
            private WindowManager.LayoutParams mParams;
            private List<View> mViews = new ArrayList<>();
            private long mMenuOpenDuration;
            private long mMenuCloseDuration;
            private float mAnchorRotationAngle;
    
            public Converter(View view) {
                mAnchorView = view;
                mContext = mAnchorView.getContext();
                if (!(mContext instanceof Activity)) {
                    throw new IllegalArgumentException("请确保当前的View依附的是Activity");
                }
            }
    
            public Converter setToggleDuration(long open, long close) {
                mMenuOpenDuration = open;
                mMenuCloseDuration = close;
                return this;
            }
    
            public Converter setAnchorRotationAngle(float angle) {
                mAnchorRotationAngle = angle;
                return this;
            }
    
            public Converter addMenuItem(@DrawableRes int iconRes, CharSequence text, final View.OnClickListener listener) {
                // 初始化Item的容器
                LinearLayout itemContainer = new LinearLayout(mContext);
                itemContainer.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT
                        , LayoutParams.WRAP_CONTENT));
                itemContainer.setOrientation(LinearLayout.VERTICAL);
                itemContainer.setGravity(Gravity.CENTER);
                // 初始化图片
                ImageView imageView = new ImageView(mContext);
                int maxRadius = mContext.getResources().getDisplayMetrics().widthPixels / 2;
                imageView.setLayoutParams(new LinearLayout.LayoutParams(maxRadius / 5, maxRadius / 5));
                imageView.setImageResource(iconRes);
                imageView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(final View view) {
                        mBottomMenuView.closeMenu(new OnMenuClosedListener() {
                            @Override
                            @SuppressLint("NewApi")
                            public void onMenuClosed() {
                                listener.onClick(view);
                                if (mWindowContainer.isAttachedToWindow()) {
                                    mWindowManager.removeView(mWindowContainer);
                                }
                            }
                        });
                    }
                });
                itemContainer.addView(imageView);
                // 初始化文本
                if (text != null && !((String) text).trim().isEmpty()) {
                    TextView textView = new TextView(mContext);
                    textView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
                            LayoutParams.WRAP_CONTENT));
                    textView.setText(text);
                    textView.setTextColor(Color.BLACK);
                    textView.setPadding(0, dp2px(5), 0, 0);
                    itemContainer.addView(textView);
                }
                mViews.add(itemContainer);
                return this;
            }
    
            public BottomSectorMenuView apply() {
                init();
                // 开启时, 将我们的Container加载到Window中
                mAnchorView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 先添加进Window
                        mWindowManager.addView(mWindowContainer, mParams);
                        // 再开启菜单
                        mBottomMenuView.openMenu();
                    }
                });
                // 给mContainer绑定返回监听器
                mWindowContainer.setOnBackPressedListener(new ContainerView.OnBackPressedListener() {
                    @Override
                    public void onBackPress() {
                        if (mBottomMenuView.isOpen()) {
                            mBottomMenuView.closeMenu(null);
                        }
                    }
                });
                mAnchorView.post(new Runnable() {
                    @Override
                    public void run() {
                        mBottomMenuView.setToggleDuration(mMenuOpenDuration, mMenuCloseDuration);
                        mBottomMenuView.setAnchorRotationAngle(mAnchorRotationAngle);
                        mBottomMenuView.initData(mAnchorView, Converter.this);
                        mBottomMenuView.addMenuItems(mViews);
                    }
                });
                return mBottomMenuView;
            }
    
            private void init() {
                mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
                mParams = new WindowManager.LayoutParams();
                mParams.width = mContext.getResources().getDisplayMetrics().widthPixels;
                mParams.height = mContext.getResources().getDisplayMetrics().heightPixels;
                mParams.flags = WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE
                        | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
                mParams.format = PixelFormat.TRANSPARENT;
                mWindowContainer = new ContainerView(mContext);
                mBottomMenuView = new BottomSectorMenuView(mContext);
                mBottomMenuView.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
                mWindowContainer.addView(mBottomMenuView);
            }
    
            @RequiresApi(api = Build.VERSION_CODES.KITKAT)
            @Override
            public void onMenuClosed() {
                if (mWindowContainer.isAttachedToWindow()) {
                    mWindowManager.removeView(mWindowContainer);
                }
            }
    
            protected int dp2px(float dp) {
                return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mContext.getResources().getDisplayMetrics());
            }
        }
    
        /**
         * 用于回调Window下的点击返回事件的ViewGroup
         */
        public static class ContainerView extends FrameLayout {
    
            private boolean mIsBackPressed = false;
    
            public ContainerView(Context context) {
                super(context);
            }
    
            public ContainerView(Context context, AttributeSet attrs) {
                super(context, attrs);
            }
    
            public ContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);
            }
    
            /**
             * 点击返回事件的回调接口
             */
            public interface OnBackPressedListener {
                void onBackPress();
            }
    
            OnBackPressedListener mOnBackPressedListener = null;
    
            public void setOnBackPressedListener(OnBackPressedListener listener) {
                mOnBackPressedListener = listener;
            }
    
            /**
             * 重写该方法, 用于监听返回键
             */
            @Override
            public boolean dispatchKeyEvent(KeyEvent event) {
                if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
                        || event.getKeyCode() == KeyEvent.KEYCODE_SETTINGS) {
                    if (mIsBackPressed) return true;// 防止返回按钮被处触发两次
                    mIsBackPressed = true;
                    if (mOnBackPressedListener != null) {
                        // 从我们定义的接口中将其回调出去
                        mOnBackPressedListener.onBackPress();
                        postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mIsBackPressed = false;
                            }
                        }, 500);
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:SectorMenuView底部导航扇形菜单

        本文链接:https://www.haomeiwen.com/subject/qaxnzxtx.html