Android自定義ViewGroup實現(xiàn)帶箭頭的圓角矩形菜單
本文和大家一起做一個帶箭頭的圓角矩形菜單,大概長下面這個樣子:
要求頂上的箭頭要對準菜單錨點,菜單項按壓反色,菜單背景色和按壓色可配置。
最簡單的做法就是讓UX給個三角形的圖片往上一貼,但是轉(zhuǎn)念一想這樣是不是太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的類,在構(gòu)造函數(shù)中初始化這些屬性
這里需要用到一個obtainStyledAttributes()方法,獲取一個TypedArray對象,然后就可以根據(jù)類型獲取相應(yīng)的屬性值了。需要注意的是該對象用完以后需要顯式調(diào)用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的寬高尺寸的。
我們先考慮一下高度:
•首先要為箭頭跟圓角預(yù)留高度,maxHeight要加上這兩項
•然后就是測量所有可見的child,ViewGroup已經(jīng)提供了現(xiàn)成的measureChild()方法
•接下來就把獲得的child的高度累加到maxHeight上,當(dāng)然還要考慮上下的margin配置
•除此以外,還需要考慮到上下的padding,以及陰影的高度
•最后通過setMeasuredDimension()設(shè)置生效
在考慮一下寬度:
•首先也是通過measureChild()方法測量所有可見的child
•然后就是比較這些child的寬度以及左右的margin配置,選最大值
•接下來還有加上左右的padding,以及陰影寬度
•最后通過setMeasuredDimension()設(shè)置生效
@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); }
看起來是不是很簡單?當(dāng)然還有兩個小問題:
1. 高度為圓角預(yù)留尺寸的時候,為什么只留了一個半徑,而不是上下兩個半徑?
其實這是從顯示效果上來考慮的,如果上下各留一個半徑,會造成菜單的邊框很厚不好看,后面實現(xiàn)onLayout()的時候你會發(fā)現(xiàn),我們布局菜單項的時候會往上移半個半徑,這樣邊框看起來就好看多了。
2. Child的布局參數(shù)為什么可以強轉(zhuǎn)成MarginLayoutParams?
這里其實需要重寫另一個方法generateLayoutParams(),返回你想要布局參數(shù)類型。一般就是用MarginLayoutParams,當(dāng)然你也可以用其他類型或者自定義類型。
@Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }
四、重寫onLayout()方法
onLayout()方法,顧名思義,就是用來布局這個ViewGroup里的所有子View的。
實際上每個View都有一個layout()方法,我們需要做的只是把合適的left/top/right/bottom坐標傳入這個方法就可以了。
這里就可以看到,我們布局菜單項的時候往上提了半個半徑,因此topOffset只加了半個半徑,另外右側(cè)的坐標也只減了半個半徑。
@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. 繪制圓角矩形
這一步比較簡單,直接調(diào)用Canvas的drawRoundRect()就完成了。
2. 繪制三角箭頭
這個需要根據(jù)配置的屬性,設(shè)定一個路徑,然后調(diào)用Canvas的drawPath()完成繪制。
3. 繪制菜單陰影
這個說白了就是換一個顏色再畫一個圓角矩形,位置略有偏移,當(dāng)然還要有模糊效果。
要獲得模糊效果,需要通過Paint的setMaskFilter()進行配置,并且需要關(guān)閉該圖層的硬件加速,這一點在API里有明確說明。
除此以外,還需要設(shè)置源圖像和目標圖像的重疊模式,陰影顯然要疊到菜單背后,根據(jù)下圖可知,我們需要選擇DST_OVER模式。
其他細節(jié)看代碼就清楚了:
@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的實現(xiàn)已經(jīng)完成了,那我們就在項目里用一用吧!使用自定義ViewGroup和使用系統(tǒng)ViewGroup組件有兩個小區(qū)別:
一、是要指定完整的包名,否則運行的時候會報找不到該組件。
二、是配置自定義屬性的時候要需要另外指定一個名字空間,避免跟默認的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沒有什么區(qū)別了,這里主要是在創(chuàng)建彈出菜單的時候指定了剛剛那個layout XML,具體看下示例代碼就清楚了。
至此,一個完整的自定義ViewGroup的流程就算走了一遍了,后面有時間可能還會寫一些復(fù)雜一些的自定義組件,但是萬變不離其宗,基本的原理跟步驟都是相同的。本文就是拋磚引玉,希望能給需要自定義ViewGroup的朋友一些幫助。
源碼下載:http://xiazai.jb51.net/201607/yuanma/ArrowRectangleMenu(jb51.net).rar
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android自定義ViewGroup實現(xiàn)堆疊頭像的點贊Layout
- Android自定義ViewGroup實現(xiàn)標簽浮動效果
- Android自定義ViewGroup之實現(xiàn)FlowLayout流式布局
- Android App開發(fā)中自定義View和ViewGroup的實例教程
- 一篇文章弄懂Android自定義viewgroup的相關(guān)難點
- Android應(yīng)用開發(fā)中自定義ViewGroup的究極攻略
- Android自定義ViewGroup實現(xiàn)受邊界限制的滾動操作(3)
- Android動畫效果之自定義ViewGroup添加布局動畫(五)
- Android自定義ViewGroup的實現(xiàn)方法
- Android自定義ViewGroup實現(xiàn)朋友圈九宮格控件
相關(guān)文章
Android編程獲取設(shè)備MAC地址的實現(xiàn)方法
這篇文章主要介紹了Android編程獲取設(shè)備MAC地址的實現(xiàn)方法,涉及Android針對硬件設(shè)備的操作技巧,需要的朋友可以參考下2017-01-01android中soap協(xié)議使用(ksoap調(diào)用webservice)
kSOAP是如何調(diào)用ebservice的呢,首先要使用SoapObject,這是一個高度抽象化的類,完成SOAP調(diào)用??梢哉{(diào)用它的addProperty方法填寫要調(diào)用的webservice方法的參數(shù)2014-02-02Android自定義View實現(xiàn)廣告信息上下滾動效果
這篇文章主要為大家詳細介紹了Android自定義View實現(xiàn)廣告信息上下滾動的具體代碼,感興趣的小伙伴們可以參考一下2016-05-05Android仿抖音右滑清屏左滑列表功能的實現(xiàn)代碼
這篇文章主要介紹了Android仿抖音右滑清屏左滑列表功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06Android基于widget組件實現(xiàn)物體移動/控件拖動功能示例
這篇文章主要介紹了Android基于widget組件實現(xiàn)物體移動/控件拖動功能,結(jié)合實例形式分析了widget組件在桌面應(yīng)用中的事件響應(yīng)與屬性動態(tài)操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2016-10-10學(xué)習(xí)Android自定義Spinner適配器
這篇文章主要為大家詳細介紹了學(xué)習(xí)Android自定義Spinner適配器的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-05-05Android RecyclerView仿新聞頭條的頻道管理功能
這篇文章主要介紹了Android RecyclerView仿新聞頭條的頻道管理功能,需要的朋友可以參考下2017-06-06