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

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

2023年7月15日发(作者:)

Android⾃定义ViewGroup实现带箭头的圆⾓矩形菜单本⽂和⼤家⼀起做⼀个带箭头的圆⾓矩形菜单,⼤概长下⾯这个样⼦:要求顶上的箭头要对准菜单锚点,菜单项按压反⾊,菜单背景⾊和按压⾊可配置。最简单的做法就是让UX给个三⾓形的图⽚往上⼀贴,但是转念⼀想这样是不是太low了点,⽽且不同分辨率也不太好适配,⼲脆⾃定义⼀个ViewGroup吧!⾃定义ViewGroup其实很简单,基本都是按⼀定的套路来的。

⼀、定义⼀个就是声明⼀下你的这个⾃定义View有哪些可配置的属性,将来使⽤的时候可以⾃由配置。这⾥声明了7个属性,分别是:箭头宽度、箭头⾼度、箭头⽔平偏移、圆⾓半径、菜单背景⾊、阴影⾊、阴影厚度。

⼆、写⼀个继承ViewGroup的类,在构造函数中初始化这些属性这⾥需要⽤到⼀个obtainStyledAttributes()⽅法,获取⼀个TypedArray对象,然后就可以根据类型获取相应的属性值了。需要注意的是该对象⽤完以后需要显式调⽤recycle()⽅法释放掉。 public class ArrowRectangleView extends ViewGroup { ... ... public ArrowRectangleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = me().obtainStyledAttributes(attrs, ectangleView, defStyleAttr, 0); for (int i = 0; i < exCount(); i++) { int attr = ex(i); switch (attr) { case ectangleView_arrow_width: mArrowWidth = ensionPixelSize(attr, mArrowWidth); break; case ectangleView_arrow_height: mArrowHeight = ensionPixelSize(attr, mArrowHeight); break; case ectangleView_radius: mRadius = ensionPixelSize(attr, mRadius); break; case ectangleView_background_color: mBackgroundColor = or(attr, mBackgroundColor); break; case ectangleView_arrow_offset: mArrowOffset = ensionPixelSize(attr, mArrowOffset); break; case ectangleView_shadow_color: mShadowColor = or(attr, mShadowColor); break; case ectangleView_shadow_thickness: mShadowThickness = ensionPixelSize(attr, mShadowThickness); break; } } e(); }

三、重写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) outParams(); if (ibility() != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); maxWidth = (maxWidth, suredWidth() + rgin + argin); maxHeight = maxHeight + suredHeight() + gin + Margin; } } maxWidth = maxWidth + getPaddingLeft() + getPaddingRight() + mShadowThickness; maxHeight = maxHeight + getPaddingTop() + getPaddingBottom() + mShadowThickness; setMeasuredDimension(maxWidth, maxHeight); }

看起来是不是很简单?当然还有两个⼩问题:1. ⾼度为圆⾓预留尺⼨的时候,为什么只留了⼀个半径,⽽不是上下两个半径?其实这是从显⽰效果上来考虑的,如果上下各留⼀个半径,会造成菜单的边框很厚不好看,后⾯实现onLayout()的时候你会发现,我们布局菜单项的时候会往上移半个半径,这样边框看起来就好看多了。2. Child的布局参数为什么可以强转成MarginLayoutParams?这⾥其实需要重写另⼀个⽅法generateLayoutParams(),返回你想要布局参数类型。⼀般就是⽤MarginLayoutParams,当然你也可以⽤其他类型或者⾃定义类型。 @Override public Params 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 * suredHeight(); bottom = top + suredHeight(); (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(_TYPE_SOFTWARE, null); Paint paint = new Paint(); iAlias(true); or(mBackgroundColor); le(); // set Xfermode for source and shadow overlap rmode(new PorterDuffXfermode(_OVER)); // draw round corner rectangle or(mBackgroundColor); undRect(new RectF(0, mArrowHeight, getMeasuredWidth() - mShadowThickness, getMeasuredHeight() - mShadowThickness), mRadius, mRadius, paint); // draw arrow Path path = new Path(); int startPoint = getMeasuredWidth() - mArrowOffset; (startPoint, mArrowHeight); (startPoint + mArrowWidth, mArrowHeight); (startPoint + mArrowWidth / 2, 0); (); th(path, paint); // draw shadow if (mShadowThickness > 0) { kFilter(new BlurMaskFilter(mShadowThickness, )); or(mShadowColor); undRect(new RectF(mShadowThickness, mArrowHeight + mShadowThickness, getMeasuredWidth() - mShadowThickness, getMeasuredHeight() - mShadowThickness), mRadius, mRadius, paint); } chDraw(canvas); }

六、在layout XML中引⽤该⾃定义ViewGroup到此为⽌,⾃定义ViewGroup的实现已经完成了,那我们就在项⽬⾥⽤⼀⽤吧!使⽤⾃定义ViewGroup和使⽤系统ViewGroup组件有两个⼩区别:⼀、是要指定完整的包名,否则运⾏的时候会报找不到该组件。

⼆、是配置⾃定义属性的时候要需要另外指定⼀个名字空间,避免跟默认的android名字空间混淆。⽐如这⾥就指定了⼀个新的app名字空间来引⽤⾃定义属性。

七、在代码⾥引⽤该layout XML这个就跟引⽤正常的layout XML没有什么区别了,这⾥主要是在创建弹出菜单的时候指定了刚刚那个layout XML,具体看下⽰例代码就清楚了。

⾄此,⼀个完整的⾃定义ViewGroup的流程就算⾛了⼀遍了,后⾯有时间可能还会写⼀些复杂⼀些的⾃定义组件,但是万变不离其宗,基本的原理跟步骤都是相同的。本⽂就是抛砖引⽟,希望能给需要⾃定义ViewGroup的朋友⼀些帮助。以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。

发布者:admin,转转请注明出处:http://www.yc00.com/web/1689428951a246703.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信