Android自定义ViewGroup(一)——带箭头的圆角矩形菜单

今天要做一个带箭头的圆角矩形菜单,大概长下面这个样子:

要求顶上的箭头要对准菜单锚点,菜单项按压反色,菜单背景色和按压色可配置。

最简单的做法就是让UX给个三角形的图片往上一贴,但是转念一想这样是不是太low了点,而且不同分辨率也不太好适配,干脆自定义一个ViewGroup吧!

自定义ViewGroup其实很简单,基本都是按一定的套路来的。

一、定义一个attrs.xml

就是声明一下你的这个自定义View有哪些可配置的属性,将来使用的时候可以自由配置。这里声明了7个属性,分别是:箭头宽度、箭头高度、箭头水平偏移、圆角半径、菜单背景色、阴影色、阴影厚度。

<resources>
    <declare-styleable name="ArrowRectangleView">
        <attr name="arrow_width" format="dimension" />
        <attr name="arrow_height" format="dimension" />
        <attr name="arrow_offset" format="dimension" />
        <attr name="radius" format="dimension" />
        <attr name="background_color" format="color" />
        <attr name="shadow_color" format="color" />
        <attr name="shadow_thickness" format="dimension" />
    </declare-styleable>
</resources>

二、写一个继承ViewGroup的类,在构造函数中初始化这些属性

这里需要用到一个obtainStyledAttributes()方法,获取一个TypedArray对象,然后就可以根据类型获取相应的属性值了。需要注意的是该对象用完以后需要显式调用recycle()方法释放掉。

public class ArrowRectangleView extends ViewGroup {
    ... ...
    public ArrowRectangleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.ArrowRectangleView, defStyleAttr, 0);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.ArrowRectangleView_arrow_width:
                    mArrowWidth = a.getDimensionPixelSize(attr, mArrowWidth);
                    break;
                case R.styleable.ArrowRectangleView_arrow_height:
                    mArrowHeight = a.getDimensionPixelSize(attr, mArrowHeight);
                    break;
                case R.styleable.ArrowRectangleView_radius:
                    mRadius = a.getDimensionPixelSize(attr, mRadius);
                    break;
                case R.styleable.ArrowRectangleView_background_color:
                    mBackgroundColor = a.getColor(attr, mBackgroundColor);
                    break;
                case R.styleable.ArrowRectangleView_arrow_offset:
                    mArrowOffset = a.getDimensionPixelSize(attr, mArrowOffset);
                    break;
                case R.styleable.ArrowRectangleView_shadow_color:
                    mShadowColor = a.getColor(attr, mShadowColor);
                    break;
                case R.styleable.ArrowRectangleView_shadow_thickness:
                    mShadowThickness = a.getDimensionPixelSize(attr, mShadowThickness);
                    break;
            }
        }
        a.recycle();
    }

三、重写onMeasure()方法

onMeasure()方法,顾名思义,就是用来测量你这个ViewGroup的宽高尺寸的。

我们先考虑一下高度:

  • 首先要为箭头跟圆角预留高度,maxHeight要加上这两项
  • 然后就是测量所有可见的child,ViewGroup已经提供了现成的measureChild()方法
  • 接下来就把获得的child的高度累加到maxHeight上,当然还要考虑上下的margin配置
  • 除此以外,还需要考虑到上下的padding,以及阴影的高度
  • 最后通过setMeasuredDimension()设置生效

在考虑一下宽度:

  • 首先也是通过measureChild()方法测量所有可见的child
  • 然后就是比较这些child的宽度以及左右的margin配置,选最大值
  • 接下来还有加上左右的padding,以及阴影宽度
  • 最后通过setMeasuredDimension()设置生效
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        int maxWidth = 0;
        // reserve space for the arrow and round corners
        int maxHeight = mArrowHeight + mRadius;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = maxHeight + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }
        }

        maxWidth = maxWidth + getPaddingLeft() + getPaddingRight() + mShadowThickness;
        maxHeight = maxHeight + getPaddingTop() + getPaddingBottom() + mShadowThickness;

        setMeasuredDimension(maxWidth, maxHeight);
    }

看起来是不是很简单?当然还有两个小问题:

1. 高度为圆角预留尺寸的时候,为什么只留了一个半径,而不是上下两个半径?

其实这是从显示效果上来考虑的,如果上下各留一个半径,会造成菜单的边框很厚不好看,后面实现onLayout()的时候你会发现,我们布局菜单项的时候会往上移半个半径,这样边框看起来就好看多了。

2. Child的布局参数为什么可以强转成MarginLayoutParams?

这里其实需要重写另一个方法generateLayoutParams(),返回你想要布局参数类型。一般就是用MarginLayoutParams,当然你也可以用其他类型或者自定义类型。

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

四、重写onLayout()方法

onLayout()方法,顾名思义,就是用来布局这个ViewGroup里的所有子View的。

实际上每个View都有一个layout()方法,我们需要做的只是把合适的left/top/right/bottom坐标传入这个方法就可以了。

这里就可以看到,我们布局菜单项的时候往上提了半个半径,因此topOffset只加了半个半径,另外右侧的坐标也只减了半个半径。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int topOffset = t + mArrowHeight + mRadius/2;
        int top = 0;
        int bottom = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            top = topOffset + i * child.getMeasuredHeight();
            bottom = top + child.getMeasuredHeight();
            child.layout(l, top, r - mRadius/2 - mShadowThickness, bottom);
        }
    }

五、重写dispatchDraw()方法

这里因为我们是写了一个ViewGroup容器,本身是不需要绘制的,因此我们就需要重写它的dispatchDraw()方法。如果你重写的是一个具体的View,那也可以重写它的onDraw()方法。

绘制过程分为三步:

1. 绘制圆角矩形

这一步比较简单,直接调用Canvas的drawRoundRect()就完成了。

2. 绘制三角箭头

这个需要根据配置的属性,设定一个路径,然后调用Canvas的drawPath()完成绘制。

3. 绘制菜单阴影

这个说白了就是换一个颜色再画一个圆角矩形,位置略有偏移,当然还要有模糊效果。

要获得模糊效果,需要通过Paint的setMaskFilter()进行配置,并且需要关闭该图层的硬件加速,这一点在API里有明确说明。

除此以外,还需要设置源图像和目标图像的重叠模式,阴影显然要叠到菜单背后,根据下图可知,我们需要选择DST_OVER模式。

其他细节看代码就清楚了:

@Override
    protected void dispatchDraw(Canvas canvas) {
        // disable h/w acceleration for blur mask filter
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(mBackgroundColor);
        paint.setStyle(Paint.Style.FILL);

        // set Xfermode for source and shadow overlap
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));

        // draw round corner rectangle
        paint.setColor(mBackgroundColor);
        canvas.drawRoundRect(new RectF(0, mArrowHeight, getMeasuredWidth() - mShadowThickness, getMeasuredHeight() - mShadowThickness), mRadius, mRadius, paint);

        // draw arrow
        Path path = new Path();
        int startPoint = getMeasuredWidth() - mArrowOffset;
        path.moveTo(startPoint, mArrowHeight);
        path.lineTo(startPoint + mArrowWidth, mArrowHeight);
        path.lineTo(startPoint + mArrowWidth / 2, 0);
        path.close();
        canvas.drawPath(path, paint);

        // draw shadow
        if (mShadowThickness > 0) {
            paint.setMaskFilter(new BlurMaskFilter(mShadowThickness, BlurMaskFilter.Blur.OUTER));
            paint.setColor(mShadowColor);
            canvas.drawRoundRect(new RectF(mShadowThickness, mArrowHeight + mShadowThickness, getMeasuredWidth() - mShadowThickness, getMeasuredHeight() - mShadowThickness), mRadius, mRadius, paint);
        }

        super.dispatchDraw(canvas);
    }

六、在layout XML中引用该自定义ViewGroup

到此为止,自定义ViewGroup的实现已经完成了,那我们就在项目里用一用吧!使用自定义ViewGroup和使用系统ViewGroup组件有两个小区别:

一是要指定完整的包名,否则运行的时候会报找不到该组件。

二是配置自定义属性的时候要需要另外指定一个名字空间,避免跟默认的android名字空间混淆。比如这里就指定了一个新的app名字空间来引用自定义属性。

<?xml version="1.0" encoding="utf-8"?>
<com.xinxin.arrowrectanglemenu.widget.ArrowRectangleView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="@android:color/transparent"
        android:paddingLeft="3dp"
        android:paddingRight="3dp"
        android:splitMotionEvents="false"
        app:arrow_offset="31dp"
        app:arrow_width="16dp"
        app:arrow_height="8dp"
        app:radius="5dp"
        app:background_color="#ffb1df83"
        app:shadow_color="#66000000"
        app:shadow_thickness="5dp">
    <LinearLayout
        android:id="@+id/cmx_toolbar_menu_turn_off"
        android:layout_width="wrap_content"
        android:layout_height="42dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="16sp"
            android:textColor="#FF393F4A"
            android:paddingLeft="16dp"
            android:paddingRight="32dp"
            android:clickable="false"
            android:text="Menu Item #1"/>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/cmx_toolbar_menu_feedback"
        android:layout_width="wrap_content"
        android:layout_height="42dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="16sp"
            android:textColor="#FF393F4A"
            android:paddingLeft="16dp"
            android:paddingRight="32dp"
            android:clickable="false"
            android:text="Menu Item #2"/>
    </LinearLayout>
</com.xinxin.arrowrectanglemenu.widget.ArrowRectangleView>

七、在代码里引用该layout XML

这个就跟引用正常的layout XML没有什么区别了,这里主要是在创建弹出菜单的时候指定了刚刚那个layout XML,具体看下示例代码就清楚了。

至此,一个完整的自定义ViewGroup的流程就算走了一遍了,后面有时间可能还会写一些复杂一些的自定义组件,但是万变不离其宗,基本的原理跟步骤都是相同的。本文就是抛砖引玉,希望能给需要自定义ViewGroup的朋友一些帮助。

示例代码下载(CSDN)

https://github.com/qianxin2016/ArrowRectangleMenu

时间: 2024-12-06 05:41:06

Android自定义ViewGroup(一)——带箭头的圆角矩形菜单的相关文章

Android自定义进度条-带文本(文字进度)的水平进度条(ProgressBar)

/** * 带文本提示的进度条 */ public class TextProgressBar extends ProgressBar { private String text; private Paint mPaint; public TextProgressBar(Context context) { super(context); initText(); } public TextProgressBar(Context context, AttributeSet attrs, int d

Android 自定义ViewGroup之实现FlowLayout-标签流容器

本篇文章讲的是Android 自定义ViewGroup之实现标签流式布局-FlowLayout,开发中我们会经常需要实现类似于热门标签等自动换行的流式布局的功能,网上也有很多这样的FlowLayout,但不影响我对其的学习.和往常一样,主要还是想总结一下自定义ViewGroup的开发过程以及一些需要注意的地方. 按照惯例,我们先来看看效果图 一.写代码之前,有几个是问题是我们先要弄清楚的: 1.什么是ViewGroup:从名字上来看,它可以被翻译为控件组,言外之意是ViewGroup内部包含了许

Android 自定义EditText输入框 带清空按钮

总结  Android 自定义EditText输入框 带清空按钮 当用户输入字符后  EditText会自动在输入框的内部右侧出现删除按钮 重写EditText达到简化布局的效果 效果图: 继承EditText package com.example.myedittexttest; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; imp

我的Android进阶之旅------&gt;Android自定义View实现带数字的进度条(NumberProgressBar)

今天在Github上面看到一个来自于 daimajia所写的关于Android自定义View实现带数字的进度条(NumberProgressBar)的精彩案例,在这里分享给大家一起来学习学习!同时感谢daimajia的开源奉献! 第一步.效果展示 图1.蓝色的进度条 图2.红色的进度条 图3.多条颜色不同的进度条 图4.多条颜色不同的进度条 版权声明:本文为[欧阳鹏]原创文章,欢迎转载,转载请注明出处! [http://blog.csdn.net/ouyang_peng/article/deta

Android 自定义ViewGroup手把手教你实现ArcMenu

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37567907 逛eoe发现这样的UI效果,感觉很不错,后来知道github上有这么个开源项目~~~~当然本篇不是教你如何使用这个开源项目,而是教你如何自己通过自定义ViewGroup写这样的效果,自定义ViewGroup也是我的痛楚,嘿嘿,希望以此可以抛砖引玉~~ 效果图: 1.实现思路 通过效果图,会有几个问题: a.动画效果如何实现 可以看出动画是从顶点外外发射的,可能有人

Android 自定义ViewGroup 实战篇 -&gt; 实现FlowLayout

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38352503 ,本文出自[张鸿洋的博客] 1.概述 上一篇已经基本给大家介绍了如何自定义ViewGroup,如果你还不了解,请查看:Android 手把手教您自定ViewGroup ,本篇将使用上篇介绍的方法,给大家带来一个实例:实现FlowLayout,何为FlowLayout,如果对Java的Swing比较熟悉的话一定不会陌生,就是控件根据ViewGroup的宽,自动的往右

android自定义viewgroup实现等分格子布局

先上效果图: 实现这样的效果: 一般的思路就是,直接写布局文件,用LinearLayout 嵌套多层子LinearLayout,然后根据权重layout_weight可以达到上面的效果 还有就是利用gridview了,但是这里的需求就是不能上下滑动,使用gridview的时候还要计算布局的高度,否则内容超出下滑: 开始我是用的第一种,直接在布局文件实现了,但是后来发现代码太多太恶心哦,所以我继承viewGroup,重写两个关键的方法:onLayout(),onMeasure() 我的大致思路:

Android自定义ViewGroup (选择照片或者拍照)

教你搞定Android自定义ViewGroup http://www.jianshu.com/p/138b98095778 字数1794 阅读7030 评论8 喜欢37 上一篇我们介绍了Android中自定义View的知识,并实现了一个类似Google彩虹进度条的自定义View,今天我们将进一步学习如何去自定义一个ViewGroup. ViewGroup 我们知道ViewGroup就是View的容器类,我们经常用的LinearLayout,RelativeLayout等都是ViewGroup的子

【Android自定义ViewGroup】不一样的轮子,巧用类变量解决冲突,像IOS那样简单的使用侧滑删除,一个控件搞定Android item侧滑删除菜单。

================================================================================== [1 序言] 侧滑删除的轮子网上有很多,最初在github上看过一个,还是ListView时代,那是一个自定义ListView 实现侧滑删除的,当初就觉得这种做法不是最佳,万一我项目里又同时有自定义ListView的需求,会增加复杂度. 写这篇文章之前又通过毒度搜了一下,排名前几的CSDN文章,都是通过自定义ListVIew和Vie