首頁 > Java > java教程 > 主體

Android自訂ViewGroup實作帶箭頭的圓角矩形選單

高洛峰
發布: 2017-01-16 17:02:11
原創
1716 人瀏覽過

本文和大家一起做一個帶箭頭的圓角矩形菜單,大概長下面這個樣子: 

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

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

更多Android自訂ViewGroup實作帶箭頭的圓角矩形選單相关文章请关注PHP中文网!


來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!