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的朋友一些帮助。

源码下载:http://xiazai.jb51.net/201607/yuanma/ArrowRectangleMenu(jb51.net).rar

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

(0)

相关推荐

  • Android自定义View实现箭头沿圆转动实例代码

    具体代码如下所示: //MyCircleView类 public class MyCircleView extends View{ //当前画笔画圆的颜色 private int CurrenCircleBoundColor; private Paint paint; ////从xml中获取的颜色 private int circleBundColor; private float circleBoundWidth; private float pivotX; private float piv

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

    本文和大家一起做一个带箭头的圆角矩形菜单,大概长下面这个样子: 要求顶上的箭头要对准菜单锚点,菜单项按压反色,菜单背景色和按压色可配置. 最简单的做法就是让UX给个三角形的图片往上一贴,但是转念一想这样是不是太low了点,而且不同分辨率也不太好适配,干脆自定义一个ViewGroup吧! 自定义ViewGroup其实很简单,基本都是按一定的套路来的. 一.定义一个attrs.xml 就是声明一下你的这个自定义View有哪些可配置的属性,将来使用的时候可以自由配置.这里声明了7个属性,分别是:箭头宽

  • Android自定义View实现带4圆角或者2圆角的效果

    1 问题 实现任意view经过自定义带4圆角或者2圆角的效果 2 原理 1) 实现view 4圆角 我们只需要把左边的图嵌入到右边里面去,最终显示左边的图就行. 2) 实现view上2圆角 我们只需要把左边的图嵌入到右边里面去,最终显示左边的图就行. 安卓源码里面有这样的类 package android.graphics; /** * <p>Specialized implementation of {@link Paint}'s * {@link Paint#setXfermode(Xfe

  • Android自定义ViewGroup实现朋友圈九宫格控件

    目录 一.简介 1.1.效果图如下 1.2.主要功能如下 二.使用 2.1.自定义属性如下 2.2.布局中使用自定义NineImageLayout 2.3.Adapter方式绑定数据和UI 2.4.列表里面使用 三.源码地址 四.总结 一.简介 最近项目里有个类似微信朋友圈的九图控件的需求,Github找了一下,发现都不太满足需求,我需要单张图片的时候可以按照图片宽高比列在一定范围内自适应,而大多开源项目单张图片也是一个小正方形,所以,干脆自己动手写一个 1.1.效果图如下 1.2.主要功能如下

  • Android自定义ViewGroup实现右滑进入详情

    目录 前言 一.抖音直接右滑进入详情 二.闲鱼右滑进入详情 三.列表的右滑进入详情 后记 前言 在之前的 ViewGroup 的事件相关一文中,我们详细的讲解了一些常见的 ViewGroup 需要处理的事件与运动的方式. 我们了解了如何处理拦截事件,如何滚动,如何处理子 View 的协调运动等. 再复杂一点,我们可以组合在一起使用.例如在拦截事件之后滚动,或者在滚动到一个阈值之后拦截事件. 今天我们一起再巩固一下相关的知识点,以比较常见的一个应用场景,右滑进入详情的场景为例子. 这个例子中又分几

  • Android自定义ViewGroup之FlowLayout(三)

    本篇继续来讲自定义ViewGroup,给大家带来一个实例:FlowLayout.何为FlowLayout,就是控件根据ViewGroup的宽,自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行,所以也叫流式布局.Android并没有提供流式布局,但是某些场合中,流式布局还是非常适合使用的,比如关键字标签,搜索热词列表等,比如下图: 定义FlowLayout LayoutParams,onLayout的写法都和上一篇讲WaterfallLayout一模一样,在此不再赘述了,没看过的可以参照

  • Android自定义View实现带数字的进度条实例代码

    第一步.效果展示 图1.蓝色的进度条 图2.红色的进度条 图3.多条颜色不同的进度条 图4.多条颜色不同的进度条 第二步.自定义ProgressBar实现带数字的进度条 0.项目结构 如上图所示:library项目为自定义的带数字的进度条NumberProgressBar的具体实现,demo项目为示例项目以工程依赖的方式引用library项目,然后使用自定义的带数字的进度条NumberProgressBar来做展示 如上图所示:自定义的带数字的进度条的library项目的结构图 如上图所示:de

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

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

  • Android编程实现带渐变效果的圆角矩形示例

    本文实例讲述了Android编程实现带渐变效果的圆角矩形.分享给大家供大家参考,具体如下: /** * 带渐变色效果的圆角矩形 * * @description: * @author ldm * @date 2016-4-26 下午3:47:12 */ public class RoundRectsActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onC

  • Android自定义ViewGroup实现绚丽的仿支付宝咻一咻雷达脉冲效果

    去年春节的时候支付宝推行的集福娃活动着实火的不能再火了,更给力的是春晚又可以全民参与咻一咻集福娃活动,集齐五福就可平分亿元大红包,只可惜没有敬业福--那时候在家没事写了个咻一咻插件,只要到了咻一咻的时间点插件就可以自动的点击咻一咻来咻红包,当时只是纯粹练习这部分技术代码没有公开,后续计划写篇关于插件这方面的文章,扯远了(*^__^*) --我们知道在支付宝的咻一咻页面有个雷达扩散的动画效果,当时感觉动画效果非常棒,于是私下尝试着实现了类似的效果,后来在github发现有大神也写有类似效果,于是读

  • Android自定义ViewGroup打造各种风格的SlidingMenu

    上篇给大家介绍QQ5.0侧滑菜单的视频课程,对于侧滑的时的动画效果的实现有了新的认识,似乎打通了任督二脉,目前可以实现任意效果的侧滑菜单了,感谢鸿洋大大!! 用的是HorizontalScrollView来实现的侧滑菜单功能,HorizontalScrollView的好处是为我们解决了滑动功能,处理了滑动冲突问题,让我们使用起来非常方便,但是滑动和冲突处理都是android中的难点,是我们应该掌握的知识点,掌握了这些,我们可以不依赖于系统的API,随心所欲打造我们想要的效果,因此这篇文章我将直接

随机推荐