ホームページ  >  記事  >  Java  >  Android カスタム ViewGroup は矢印付きの角丸長方形メニューを実装します

Android カスタム ViewGroup は矢印付きの角丸長方形メニューを実装します

高洛峰
高洛峰オリジナル
2017-01-16 17:02:111717ブラウズ

この記事では、矢印が付いた角の丸い長方形のメニューを作成しましょう。おそらく次のようになります:

Android カスタム ViewGroup は矢印付きの角丸長方形メニューを実装します

では、上部の矢印がメニューのアンカーポイントに揃えられている必要があり、メニュー項目は反転色を押し、メニューは背景色とプレス色を設定可能。
最も簡単な方法は、UX に三角形の画像を上向きに投稿させることですが、これでは低すぎるのではないかと考えました。また、異なる解像度に適応するのは簡単ではないため、ViewGroup をカスタマイズすることも考えます。
ViewGroup のカスタマイズは実際には非常に簡単で、基本的には特定のルーチンに従います。

1. attrs.xml
を定義すると、カスタム ビューの構成可能な属性が宣言され、将来使用するときに自由に構成できます。ここでは、矢印の幅、矢印の高さ、矢印の水平オフセット、フィレット半径、メニューの背景色、影の色、および影の太さの 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 を継承するクラスを作成し、コンストラクターでこれらの属性を初期化します
ここでは、obtainStyledAttributes() メソッドを使用して TypedArray オブジェクトを取得する必要があり、その後、対応する属性値を取得できます。種類に応じて。オブジェクトを使い切った後は、明示的に 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. onMeasure() メソッドを書き直す

onMeasure() メソッドは、名前が示すように、ViewGroup の幅と高さを測定するために使用されます。

まず高さを考えてみましょう:
• まず、矢印と丸い角の高さを予約し、これら 2 つの項目を maxHeight に追加します
• 次に、ViewGroup には既製のmeasureChild() メソッドが用意されています。 , 取得した子の高さをmaxHeightに加えます。もちろん、上下のマージンの設定も考慮する必要があります
•さらに、影の高さだけでなく、上下のパディングも考慮する必要があります
•最後に setMeasuredDimension() で設定して有効にします

幅を検討します:

•まず、measureChild() メソッドを通して、表示されているすべての子を測定します
•次に、これらの子の幅と左右のマージン設定を比較し、最大値
•次に左右のパディングを追加し、影の幅を追加します
•最後に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) 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);
}

とても簡単だと思いませんか?もちろん、小さな疑問が 2 つあります:

1. 丸い角のために高さが確保されているのに、上下に 2 つの半径ではなく、1 つの半径だけが残っているのはなぜですか?
実はこれは表示効果の観点から考えたもので、上下に半径を残すとメニューの枠線が非常に太くなり、後でonLayout()を実装した際にわかります。メニュー項目をレイアウトすると、半径の半分が上に移動するため、境界線がより良く見えます。
2. Child のレイアウト パラメーターが強制的に MarginLayoutParams に変換されるのはなぜですか?
実際には、必要なレイアウト パラメーターのタイプを返すために、ここで別のメソッド GenerateLayoutParams() を書き直す必要があります。一般的には MarginLayoutParams が使用されますが、もちろん他の型やカスタム型を使用することもできます。

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
  return new MarginLayoutParams(getContext(), attrs);
}

4. onLayout() メソッドを書き換えます

onLayout() メソッドは、名前が示すように、この ViewGroup 内のすべてのサブビューをレイアウトするために使用されます。
実際、各ビューにはlayout()メソッドがあります。必要なのは、適切な左/上/右/下の座標をこのメソッドに渡すことだけです。
ここで、メニュー項目をレイアウトするときに半径の半分を上に上げたので、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 * child.getMeasuredHeight();
    bottom = top + child.getMeasuredHeight();
    child.layout(l, top, r - mRadius/2 - mShadowThickness, bottom);
  }
}

5.dispatchDraw()メソッドを書き換えます

ここでは、ViewGroupコンテナを作成したので描画する必要がないため、そのdispatchDraw()メソッドを書き直す必要があります。特定のビューをオーバーライドする場合は、その onDraw() メソッドをオーバーライドすることもできます。
描画プロセスは3つのステップに分かれています:
1.角丸長方形を描画します
このステップは比較的単純で、CanvasのdrawRoundRect()を呼び出すだけで完了します。
2. 三角形の矢印を描画します
これには、構成されたプロパティに従ってパスを設定し、描画を完了するために Canvas のdrawPath() を呼び出す必要があります。
3. メニューの影を描く
簡単に言うと、色を変えて角丸長方形を描き、位置を少しずらして、もちろんぼかし効果も付けたものです。
ぼかし効果を取得するには、ペイントの setMaskFilter() を通じて設定する必要があり、レイヤーのハードウェア アクセラレーションをオフにする必要があります。これは API に明記されています。
さらに、ソース画像とターゲット画像のオーバーレイ モードを設定する必要があります。下の図からわかるように、影をメニューの後ろに重ねる必要があります。DST_OVER モードを選択する必要があります。

その他の詳細は、コードを見ると明らかになります:

@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 カスタム ViewGroup は矢印付きの角丸長方形メニューを実装します相关文章请关注PHP中文网!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。