Dans cet article, nous allons créer un menu rectangulaire arrondi avec des flèches, qui ressemble probablement à ceci :
Il est obligatoire que la flèche du haut soit alignée avec le point d'ancrage du menu. La couleur inverse de l'élément de menu, la couleur d'arrière-plan du menu et la couleur de la presse sont configurables.
Le plus simple est de demander à UX de poster une image triangulaire vers le haut, mais ensuite je me suis demandé si c'était trop bas, et ce n'est pas facile de s'adapter aux différentes résolutions, autant personnaliser un ViewGroup !
La personnalisation de ViewGroup est en fait très simple et suit essentiellement une certaine routine.
1. Définissez un attrs.xml
pour déclarer les attributs configurables de votre vue personnalisée, qui peuvent être librement configurés lors d'une utilisation future. Sept propriétés sont déclarées ici, à savoir : la largeur de la flèche, la hauteur de la flèche, le décalage horizontal de la flèche, le rayon du congé, la couleur d'arrière-plan du menu, la couleur de l'ombre et l'épaisseur de l'ombre.
<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. Écrivez une classe qui hérite de ViewGroup et initialisez ces attributs dans le constructeur
Ici, vous devez utiliser une méthode getStyledAttributes() pour obtenir un objet TypedArray, et alors vous pouvez obtenir la valeur d'attribut correspondante en fonction du type. Il convient de noter qu’une fois l’objet épuisé, il doit être libéré en appelant explicitement la méthode 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(); }
3. Réécrire la méthode onMeasure()
La méthode onMeasure(), comme son nom l'indique, est utilisée pour mesurez-vous Les dimensions de largeur et de hauteur de ce ViewGroup.
Considérons d'abord la hauteur :
•Tout d'abord, réservez la hauteur pour les flèches et les coins arrondis, et ajoutez ces deux éléments à maxHeight
•Mesurez ensuite tous les enfants visibles, que ViewGroup a fournis. -made MeasureChild() méthode
•Ajoutez ensuite la taille obtenue de l'enfant à maxHeight, et bien sûr considérez la configuration des marges supérieure et inférieure
•De plus, vous devez également prendre en compte le rembourrage supérieur et inférieur, comme ainsi que l'ombre La hauteur
•Réglez-la enfin via setMeasuredDimension() pour prendre effet
Considérez la largeur :
•Mesurez d'abord également tous les enfants visibles via la méthode MeasureChild()
• Comparez ensuite la largeur de ces enfants et la configuration des marges gauche et droite, sélectionnez la valeur maximale
• Ensuite, ajoutez un remplissage gauche et droit et une largeur d'ombre
• Enfin, définissez-la via setMeasuredDimension() pour prendre effet
@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); }
Ça n'a pas l'air simple ? Bien sûr, il y a deux petites questions :
1 Lorsque la hauteur est réservée au congé, pourquoi ne reste-t-il qu'un rayon au lieu de deux rayons haut et bas ?
En fait, ceci est considéré du point de vue de l'effet d'affichage. Si vous laissez un rayon en haut et en bas, la bordure du menu sera très épaisse et disgracieuse Lorsque vous implémenterez onLayout() plus tard, vous le trouverez. que lorsque nous disposerons les éléments de menu, ils monteront de la moitié du rayon pour que la bordure soit bien meilleure.
2. Pourquoi les paramètres de mise en page de Child peuvent-ils être convertis de force en MarginLayoutParams ?
Ici, vous devez en fait réécrire une autre méthode generateLayoutParams() pour renvoyer le type de paramètres de mise en page souhaité. Généralement, MarginLayoutParams est utilisé, mais bien sûr, vous pouvez également utiliser d'autres types ou types personnalisés.
@Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }
4. Réécrire la méthode onLayout()
La méthode onLayout(), comme son nom l'indique, est utilisée pour mettre en page toutes les sous-vues de ce ViewGroup.
En fait, chaque View a une méthode layout(). Tout ce que nous devons faire est de transmettre les coordonnées gauche/haut/droite/bas appropriées dans cette méthode.
Comme vous pouvez le voir ici, nous avons augmenté la moitié du rayon lors de la disposition des éléments de menu, donc topOffset n'a ajouté que la moitié du rayon, et les coordonnées sur le côté droit n'ont également diminué que la moitié du rayon.
@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. Réécrivez la méthode dispatchDraw()
Ici, parce que nous écrivons un conteneur ViewGroup, il n'a pas besoin d'être dessiné, nous devons donc le réécrire dispatchDraw. () méthode. Si vous remplacez une vue spécifique, vous pouvez également remplacer sa méthode onDraw().
Le processus de dessin est divisé en trois étapes :
1. Dessinez un rectangle arrondi
Cette étape est relativement simple, il suffit d'appeler drawRoundRect() de Canvas et elle est terminée.
2. Dessinez une flèche triangulaire
Cela nécessite de définir un chemin basé sur les attributs configurés, puis d'appeler drawPath() de Canvas pour terminer le dessin.
3. Dessiner l'ombre du menu
Pour parler franchement, il s'agit de changer la couleur et de dessiner un rectangle arrondi, avec un léger décalage dans la position, et bien sûr un effet de flou.
Pour obtenir l'effet de flou, vous devez le configurer via setMaskFilter() de Paint, et vous devez désactiver l'accélération matérielle du calque. Ceci est clairement indiqué dans l'API.
De plus, vous devez également définir le mode de superposition de l'image source et de l'image cible. L'ombre doit évidemment se chevaucher derrière le menu. Comme le montre l'image ci-dessous, nous devons sélectionner le mode DST_OVER. .
D'autres détails seront clairs en regardant le 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中文网
更多ViewGroup personnalisé Android implémente un menu rectangulaire arrondi avec des flèches相关文章请关注PHP中文网!