In this article, we will make a rounded rectangular menu with arrows, which probably looks like this:
Requires that the top arrow should be aligned with the menu anchor point. Menu item press inverse color, menu background color and press color are configurable.
The simplest way is to ask UX to post a triangular picture upwards, but then I thought about whether this is too low, and it is not easy to adapt to different resolutions, so I might as well customize a ViewGroup!
Customizing ViewGroup is actually very simple, and basically follows a certain routine.
1. Define an attrs.xml
is to declare the configurable attributes of your custom View, which can be freely configured when used in the future. Seven attributes are declared here, namely: arrow width, arrow height, arrow horizontal offset, fillet radius, menu background color, shadow color, and shadow thickness.
<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>
2. Write a class that inherits ViewGroup and initialize these attributes in the constructor
Here you need to use an obtainStyledAttributes() method to obtain a TypedArray object, and then you can Get the corresponding attribute value according to the type. It should be noted that after the object is used up, it needs to be released by explicitly calling the recycle() method.
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(); }
3. Rewrite the onMeasure() method
onMeasure() method, as the name suggests, is used to measure your ViewGroup width and height dimensions.
Let’s consider the height first:
•First, reserve the height for arrows and rounded corners, and add these two items to maxHeight
•Then measure all visible children, which ViewGroup has provided Ready-made measureChild() method
•Next, add the obtained height of the child to maxHeight, and of course consider the upper and lower margin configuration
•In addition, you also need to consider the upper and lower padding, as well as the shadow The height
•Finally set it through setMeasuredDimension() to take effect
Consider the width:
•First also measure all visible children through the measureChild() method
•Then compare these children Width and left and right margin configuration, select the maximum value
•Next, add left and right padding, and shadow width
•Finally set it through setMeasuredDimension() to take effect
@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); }
Doesn’t it look simple? Of course, there are two small questions:
1. When the height is reserved for the rounded corners, why is only one radius left instead of two radii up and down?
In fact, this is considered from the perspective of display effect. If you leave a radius at the top and bottom, the border of the menu will be very thick and unsightly. When you implement onLayout() later, you will find that when we layout the menu items, they will go up. Move half the radius so the border looks much better.
2. Why can Child's layout parameters be forcibly converted to MarginLayoutParams?
Here you actually need to override another method generateLayoutParams() to return the type of layout parameters you want. Generally, MarginLayoutParams is used, but of course you can also use other types or custom types.
@Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }
4. Rewrite the onLayout() method
The onLayout() method, as the name suggests, is used to layout all sub-Views in this ViewGroup.
In fact, each View has a layout() method. All we need to do is to pass the appropriate left/top/right/bottom coordinates into this method.
You can see here that when we laid out the menu items, we raised half the radius, so topOffset only added half the radius, and the coordinates on the right side also only reduced half the radius.
@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); } }
5. Rewrite the dispatchDraw() method
Here because we have written a ViewGroup container, it does not need to be drawn, so we need to rewrite its dispatchDraw ()method. If you are overriding a specific View, you can also override its onDraw() method.
The drawing process is divided into three steps:
1. Draw a rounded rectangle
This step is relatively simple, just call drawRoundRect() of Canvas and it is completed.
2. Draw a triangular arrow
This requires setting a path according to the configured properties, and then calling Canvas's drawPath() to complete the drawing.
3. Draw menu shadow
To put it bluntly, this is to change the color and draw a rounded rectangle, with a slightly offset position, and of course a blur effect.
To obtain the blur effect, you need to configure it through Paint's setMaskFilter(), and you need to turn off the hardware acceleration of the layer. This is clearly stated in the API.
In addition, you also need to set the overlay mode of the source image and the target image. The shadow obviously needs to be overlapped behind the menu. As shown in the figure below, we need to select the DST_OVER mode.
Other details will be clear by looking at the code:
@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 custom ViewGroup implements rounded rectangular menu with arrows相关文章请关注PHP中文网!