Android自訂ViewGroup(一)——帶箭頭的圓角矩形菜單

來源:互聯網
上載者:User

標籤:

今天要做一個帶箭頭的圓角矩形菜單,大概長下面這個樣子:


要求頂上的箭頭要對準菜單錨點,功能表項目按壓反色,菜單背景色和按壓色可配置。

最簡單的做法就是讓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

Android自訂ViewGroup(一)——帶箭頭的圓角矩形菜單

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.