专栏名称: Anlia
Android工程师
目录
相关文章推荐
开发者全社区  ·  董事长十几刀刺死 ... ·  16 小时前  
开发者全社区  ·  逆天了,OnlyFans杭州岗 ·  16 小时前  
开发者全社区  ·  55 ... ·  昨天  
开发者全社区  ·  H家最新进展 ·  2 天前  
开发者全社区  ·  没有大于40岁的P7了 ·  2 天前  
51好读  ›  专栏  ›  Anlia

Android自定义View——从零开始实现可展开收起的水平菜单栏

Anlia  · 掘金  · android  · 2017-12-23 03:37

正文

版权声明:本文为博主原创文章,未经博主允许不得转载

系列教程: Android开发之从零开始系列

源码: AnliaLee/ExpandMenu ,欢迎star

大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言 :最近项目里要实现一个 可展开收起的水平菜单栏 控件,刚接到需求时想着用自定义View自己来绘制,发现要实现 圆角、阴影、菜单滑动等效果 非常复杂且耗时间。好在这些效果 Android原生代码 中都已经有非常成熟的解决方案,我们只需要去继承它们进行 二次开发 就行。本期将教大家如何 继承ViewGroup(RelativeLayout)实现自定义菜单栏

本篇只着重于思路和实现步骤,里面用到的一些知识原理不会非常细地拿来讲,如果有不清楚的api或方法可以在网上搜下相应的资料,肯定有大神讲得非常清楚的,我这就不献丑了。本着认真负责的精神我会把相关知识的博文链接也贴出来(其实就是懒不想写那么多哈哈),大家可以自行传送。为了照顾第一次阅读系列博客的小伙伴,本篇会出现一些在之前 系列博客 就讲过的内容,看过的童鞋自行跳过该段即可

国际惯例,先上效果图


为菜单栏设置背景

自定义ViewGroup 自定义View 的绘制过程有所不同, View 可以直接在自己的 onDraw 方法中绘制所需要的效果,而 ViewGroup 会先 测量子View的大小位置(onLayout) ,然后再进行绘制,如果 子View或background为空 ,则 不会调用draw方法绘制 。当然我们可以调用 setWillNotDraw(false) ViewGroup 可以在 子View或background为空 的情况下进行绘制,但我们会为 ViewGroup 设置一个默认背景,所以可以省去这句代码

设置背景很简单,因为要实现圆角、描边等效果,所以我们选择使用 GradientDrawable 来定制背景,然后调用 setBackground 方法设置背景。创建 HorizontalExpandMenu ,继承自 RelativeLayout ,同时 自定义Attrs属性

public class HorizontalExpandMenu extends RelativeLayout {
    private Context mContext;
    private AttributeSet mAttrs;

    private int defaultWidth;//默认宽度
    private int defaultHeight;//默认长度
    private int viewWidth;
    private int viewHeight;

    private int menuBackColor;//菜单栏背景色
    private float menuStrokeSize;//菜单栏边框线的size
    private int menuStrokeColor;//菜单栏边框线的颜色
    private float menuCornerRadius;//菜单栏圆角半径

    public HorizontalExpandMenu(Context context) {
        super(context);
        this.mContext = context;
        init();
    }

    public HorizontalExpandMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        this.mAttrs = attrs;
        init();
    }

    private void init(){
        TypedArray typedArray = mContext.obtainStyledAttributes(mAttrs, R.styleable.HorizontalExpandMenu);

        defaultWidth = DpOrPxUtils.dip2px(mContext,200);
        defaultHeight = DpOrPxUtils.dip2px(mContext,40);

        menuBackColor = typedArray.getColor(R.styleable.HorizontalExpandMenu_back_color,Color.WHITE);
        menuStrokeSize = typedArray.getDimension(R.styleable.HorizontalExpandMenu_stroke_size,1);
        menuStrokeColor = typedArray.getColor(R.styleable.HorizontalExpandMenu_stroke_color,Color.GRAY);
        menuCornerRadius = typedArray.getDimension(R.styleable.HorizontalExpandMenu_corner_radius,DpOrPxUtils.dip2px(mContext,20));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = measureSize(defaultHeight, heightMeasureSpec);
        int width = measureSize(defaultWidth, widthMeasureSpec);
        viewHeight = height;
        viewWidth = width;
        setMeasuredDimension(viewWidth,viewHeight);

        //布局代码中如果没有设置background属性则在此处添加一个背景
        if(getBackground()==null){
            setMenuBackground();
        }
    }

    private int measureSize(int defaultSize, int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }

    /**
     * 设置菜单背景,如果要显示阴影,需在onLayout之前调用
     */
    private void setMenuBackground(){
        GradientDrawable gd = new GradientDrawable();
        gd.setColor(menuBackColor);
        gd.setStroke((int)menuStrokeSize, menuStrokeColor);
        gd.setCornerRadius(menuCornerRadius);
        setBackground(gd);
    }
}

attrs属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--注意这里的name要和自定义View的名称一致,不然在xml布局中无法引用-->
    <declare-styleable name="HorizontalExpandMenu">
        <attr name="back_color" format="color"></attr>
        <attr name="stroke_size" format="dimension"></attr>
        <attr name="stroke_color" format="color"></attr>
        <attr name="corner_radius" format="dimension"></attr>
    </declare-styleable>
</resources>

在布局文件中使用

<com.anlia.expandmenu.widget.HorizontalExpandMenu
	android:id="@+id/expandMenu1"
	android:layout_width="match_parent"
	android:layout_height="40dp"
	android:layout_alignParentBottom="true"
	android:layout_marginBottom="20dp"
	android:layout_marginLeft="15dp"
	android:layout_marginRight="15dp">
</com.anlia.expandmenu.widget.HorizontalExpandMenu>

效果如图


绘制菜单栏按钮

我们要绘制菜单栏两边的 按钮 ,首先是要为按钮 圈地(测量位置和大小) 。设置 按钮区域为正方形 ,位于 左侧或右侧(根据开发者设置而定) 边长和菜单栏ViewGroup的高相等 。按钮中的加号可以使用 Path 进行绘制(当然也可以用 矢量图 位图 ),代码如下

public class HorizontalExpandMenu extends RelativeLayout {
	//省略部分代码...
    private float buttonIconDegrees;//按钮icon符号竖线的旋转角度
    private float buttonIconSize;//按钮icon符号的大小
    private float buttonIconStrokeWidth;//按钮icon符号的粗细
    private int buttonIconColor;//按钮icon颜色

    private int buttonStyle;//按钮类型
    private int buttonRadius;//按钮矩形区域内圆半径
    private float buttonTop;//按钮矩形区域top值
    private float buttonBottom;//按钮矩形区域bottom值

    private Point rightButtonCenter;//右按钮中点
    private float rightButtonLeft;//右按钮矩形区域left值
    private float rightButtonRight;//右按钮矩形区域right值

    private Point leftButtonCenter;//左按钮中点
    private float leftButtonLeft;//左按钮矩形区域left值
    private float leftButtonRight;//左按钮矩形区域right值

    /**
     * 根按钮所在位置,默认为右边
     */
    public class ButtonStyle {
        public static final int Right = 0;
        public static final int Left = 1;
    }

    private void init(){
		//省略部分代码...
        buttonStyle = typedArray.getInteger(R.styleable.HorizontalExpandMenu_button_style,ButtonStyle.Right);
        buttonIconDegrees = 0;
        buttonIconSize = typedArray.getDimension(R.styleable.HorizontalExpandMenu_button_icon_size,DpOrPxUtils.dip2px(mContext,8));
        buttonIconStrokeWidth = typedArray.getDimension(R.styleable.HorizontalExpandMenu_button_icon_stroke_width,8);
        buttonIconColor = typedArray.getColor(R.styleable.HorizontalExpandMenu_button_icon_color,Color.GRAY);

        buttonIconPaint = new Paint();
        buttonIconPaint.setColor(buttonIconColor);
        buttonIconPaint.setStyle(Paint.Style.STROKE);
        buttonIconPaint.setStrokeWidth(buttonIconStrokeWidth);
        buttonIconPaint.setAntiAlias(true);

        path = new Path();
        leftButtonCenter = new Point();
        rightButtonCenter = new Point();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = measureSize(defaultHeight, heightMeasureSpec);
        int width = measureSize(defaultWidth, widthMeasureSpec);
        viewHeight = height;
        viewWidth = width;
        setMeasuredDimension(viewWidth,viewHeight);

        buttonRadius = viewHeight/2;
        layoutRootButton();

        //布局代码中如果没有设置background属性则在此处添加一个背景
        if(getBackground()==null){
            setMenuBackground();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        layoutRootButton();
        if(buttonStyle == ButtonStyle.Right){
            drawRightIcon(canvas);
        }else {
            drawLeftIcon(canvas);
        }

        super.onDraw(canvas);//注意父方法在最后调用,以免icon被遮盖
    }

    /**
     * 测量按钮中点和矩形位置
     */
    private void layoutRootButton(){
        buttonTop = 0;
        buttonBottom = viewHeight;

        rightButtonCenter.x = viewWidth- buttonRadius;
        rightButtonCenter.y = viewHeight/2;
        rightButtonLeft = rightButtonCenter.x- buttonRadius;
        rightButtonRight = rightButtonCenter.x+ buttonRadius;

        leftButtonCenter.x = buttonRadius;
        leftButtonCenter.y = viewHeight/2;
        leftButtonLeft = leftButtonCenter.x- buttonRadius;
        leftButtonRight = leftButtonCenter.x+ buttonRadius;
    }

    /**
     * 绘制左边的按钮
     * @param canvas
     */
    private void drawLeftIcon(Canvas canvas){
        path.reset();
        path.moveTo(leftButtonCenter.x- buttonIconSize, leftButtonCenter.y);
        path.lineTo(leftButtonCenter.x+ buttonIconSize, leftButtonCenter.y);
        canvas.drawPath(path, buttonIconPaint);//划横线

        canvas.save();
        canvas.rotate(-buttonIconDegrees, leftButtonCenter.x, leftButtonCenter.y);//旋转画布,让竖线可以随角度旋转
        path.reset();
        path.moveTo(leftButtonCenter.x, leftButtonCenter.y- buttonIconSize);
        path.lineTo(leftButtonCenter.x, leftButtonCenter.y+ buttonIconSize);
        canvas.drawPath(path, buttonIconPaint);//画竖线
        canvas.restore();
    }

    /**
     * 绘制右边的按钮
     * @param canvas
     */
    private void drawRightIcon(Canvas canvas){
        path.reset();
        path.moveTo(rightButtonCenter.x- buttonIconSize, rightButtonCenter.y);
        path.lineTo(rightButtonCenter.x+ buttonIconSize, rightButtonCenter.y);
        canvas.drawPath(path, buttonIconPaint);//划横线

        canvas.save();
        canvas.rotate(buttonIconDegrees, rightButtonCenter.x, rightButtonCenter.y);//旋转画布,让竖线可以随角度旋转
        path.reset();
        path.moveTo(rightButtonCenter.x, rightButtonCenter.y- buttonIconSize);
        path.lineTo(rightButtonCenter.x, rightButtonCenter.y+ buttonIconSize);
        canvas.drawPath(path, buttonIconPaint);//画竖线
        canvas.restore();
    }
}

新增attrs属性

<declare-styleable name="HorizontalExpandMenu">
	//省略部分代码...
	<attr name="button_style">
		<enum name="right" value="0"/>
		<enum name="left" value="1"/>
	</attr>
	<attr name="button_icon_size" format="dimension"></attr>
	<attr name="button_icon_stroke_width" format="dimension"></attr>
	<attr name="button_icon_color" format="color"></attr>
</declare-styleable>

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.anlia.expandmenu.widget.HorizontalExpandMenu
            android:id="@+id/expandMenu1"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="20dp"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp">
        </com.anlia.expandmenu.widget.HorizontalExpandMenu>
        <com.anlia.expandmenu.widget.HorizontalExpandMenu
            android:id="@+id/expandMenu2"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp"
            app:button_style="left">
        </com.anlia.expandmenu.widget.HorizontalExpandMenu>
    </LinearLayout>
</RelativeLayout>

效果如图


设置按钮动画与点击事件

之前我们定义了 buttonIconDegrees 属性,下面我们通过 Animation的插值器 增减 buttonIconDegrees的数值 让按钮符号可以进行变换,同时 监听Touch为按钮设置点击事件

public class HorizontalExpandMenu extends RelativeLayout {
	//省略部分代码...
    private boolean isExpand;//菜单是否展开,默认为展开
    private float downX = -1;
    private float downY = -1;
    private int expandAnimTime;//展开收起菜单的动画时间

    private void init(){
		//省略部分代码...
        buttonIconDegrees = 90;//菜单初始状态为展开,所以旋转角度为90,按钮符号为 - 号
        expandAnimTime = typedArray.getInteger(R.styleable.HorizontalExpandMenu_expand_time,400);
        isExpand = true;
        anim = new ExpandMenuAnim();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                break






请到「今天看啥」查看全文