自定义viewAndroid技术知识Android知识

SectorMenuView底部导航扇形菜单

2018-02-06  本文已影响75人  SharryChoo

这次分析一个扇形菜单展开的自定义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);
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读