In diesem Artikel erstellen wir ein abgerundetes rechteckiges Menü mit Pfeilen, das wahrscheinlich so aussieht:
Es ist erforderlich, dass der obere Pfeil mit dem ausgerichtet ist Menü-Ankerpunkt, inverse Farbe, Menü-Hintergrundfarbe und Druckfarbe sind konfigurierbar.
Der einfachste Weg ist, UX zu bitten, ein dreieckiges Bild nach oben zu posten, aber dann habe ich darüber nachgedacht, ob das zu niedrig ist und es nicht einfach ist, es an verschiedene Auflösungen anzupassen. Ich könnte genauso gut eine ViewGroup anpassen!
Das Anpassen von ViewGroup ist eigentlich sehr einfach und folgt grundsätzlich einer bestimmten Routine.
1. Definieren Sie eine attrs.xml
, um die konfigurierbaren Attribute Ihrer benutzerdefinierten Ansicht zu deklarieren, die bei zukünftiger Verwendung frei konfiguriert werden kann. Hier werden sieben Attribute deklariert, nämlich: Pfeilbreite, Pfeilhöhe, horizontaler Pfeilversatz, Verrundungsradius, Menühintergrundfarbe, Schattenfarbe und Schattenstärke.
<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. Schreiben Sie eine Klasse, die ViewGroup erbt, und initialisieren Sie diese Attribute im Konstruktor.
Hier müssen Sie eine getStyledAttributes()-Methode verwenden, um ein TypedArray-Objekt zu erhalten dann Sie können den entsprechenden Attributwert entsprechend dem Typ abrufen. Es ist zu beachten, dass das Objekt, nachdem es aufgebraucht ist, durch expliziten Aufruf der Methode recycle() freigegeben werden muss.
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. Die onMeasure()-Methode wird, wie der Name schon sagt, neu geschrieben Messen Sie die Breiten- und Höhenabmessungen dieser ViewGroup.
•Reservieren Sie zunächst die Höhe für Pfeile und abgerundete Ecken und fügen Sie diese beiden Elemente zu maxHeight hinzu
•Messen Sie dann alle sichtbaren untergeordneten Elemente, die ViewGroup bereitgestellt hat. Das steht bereit -gemachte Methode „measureChild()“•Dann addieren Sie die erhaltene Höhe des Kindes zu maxHeight und berücksichtigen Sie natürlich die Konfiguration des oberen und unteren Randes
•Darüber hinaus müssen Sie auch die obere und untere Polsterung berücksichtigen, z sowie der Schatten Die Höhe
•Stellen Sie sie abschließend über setMeasuredDimension() ein, damit sie wirksam wird
Berücksichtigen Sie die Breite:
•Messen Sie zunächst auch alle sichtbaren untergeordneten Elemente mit der Methode „measureChild()“
• Als nächstes fügen Sie den linken und rechten Abstand und die Schattenbreite hinzu
• Stellen Sie es schließlich über setMeasuredDimension() ein, damit es wirksam wird
@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); }Sieht es nicht einfach aus? Natürlich gibt es zwei kleine Fragen:
1. Wenn die Höhe für die abgerundeten Ecken reserviert ist, warum bleibt dann nur noch ein Radius übrig, anstatt zwei Radien nach oben und unten?
Tatsächlich wird dies unter dem Gesichtspunkt des Anzeigeeffekts betrachtet, dass der Rand des Menüs sehr dick und unansehnlich ist. Wenn Sie später onLayout () implementieren Wenn wir die Menüelemente anordnen, werden sie um die Hälfte des Radius verschoben, damit der Rand viel besser aussieht.2. Warum können die Layoutparameter von Child zwangsweise in MarginLayoutParams konvertiert werden? Im Allgemeinen wird MarginLayoutParams verwendet, Sie können aber natürlich auch andere Typen oder benutzerdefinierte Typen verwenden.
@Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }onLayout()-Methode wird, wie der Name schon sagt, zum Layouten aller Unteransichten in dieser ViewGroup verwendet.
Tatsächlich verfügt jede Ansicht über eine layout()-Methode. Alles, was wir tun müssen, ist, die entsprechenden linken/oben/rechts/unten-Koordinaten an diese Methode zu übergeben.
Wie Sie hier sehen können, haben wir beim Anordnen der Menüelemente den halben Radius erhöht, sodass topOffset nur den halben Radius hinzugefügt hat und die Koordinaten auf der rechten Seite auch nur den halben Radius reduziert haben.@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); } }Da wir hier einen ViewGroup-Container geschrieben haben, muss er nicht gezeichnet werden, also müssen wir ihn „dispatchDraw“ neu schreiben ()-Methode. Wenn Sie eine bestimmte Ansicht überschreiben, können Sie auch deren onDraw()-Methode überschreiben.
Der Zeichenvorgang ist in drei Schritte unterteilt:
1. Zeichnen Sie ein abgerundetes Rechteck Dieser Schritt ist relativ einfach. Rufen Sie einfach drawRoundRect() von Canvas auf und er ist abgeschlossen.
2. Zeichnen Sie einen dreieckigen Pfeil
Dazu müssen Sie einen Pfad basierend auf den konfigurierten Attributen festlegen und dann drawPath() von Canvas aufrufen, um die Zeichnung abzuschließen.
3. Menüschatten zeichnen
Um es ganz klar auszudrücken: Dies bedeutet, die Farbe zu ändern und ein abgerundetes Rechteck mit einem leichten Versatz in der Position und natürlich einem Unschärfeeffekt zu zeichnen.
Um den Unschärfeeffekt zu erzielen, müssen Sie ihn über setMaskFilter() von Paint konfigurieren und die Hardwarebeschleunigung der Ebene deaktivieren. Dies ist in der API klar angegeben.
Darüber hinaus müssen Sie auch den Überlagerungsmodus des Quellbilds und des Zielbilds festlegen. Der Schatten muss offensichtlich hinter dem Menü überlappt werden. Wie aus dem Bild unten hervorgeht, müssen wir den DST_OVER-Modus auswählen .
Weitere Details werden durch einen Blick auf den Code klar:
@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中文网
更多Die benutzerdefinierte Android-ViewGroup implementiert ein abgerundetes rechteckiges Menü mit Pfeilen相关文章请关注PHP中文网!