当前位置: 移动技术网 > IT编程>移动开发>Android > Android自定义ViewGroup实现带箭头的圆角矩形菜单

Android自定义ViewGroup实现带箭头的圆角矩形菜单

2019年07月24日  | 移动技术网IT编程  | 我要评论

欺凌尤娜2,行书书法,监控布线

本文和大家一起做一个带箭头的圆角矩形菜单,大概长下面这个样子: 


要求顶上的箭头要对准菜单锚点,菜单项按压反色,菜单背景色和按压色可配置。
最简单的做法就是让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的朋友一些帮助。

源码下载:

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网