web-dev-qa-db-ja.com

Androidでビュークリップ境界の外側に描画する場合:カスタムビューの上に基になるビューが描画されないようにするにはどうすればよいですか?

カスタムのAndroidクリッピング境界の外側に描画する必要があるビューを作成しました。

これは私が持っているものです:

enter image description here

これは、ボタンをクリックしたときに起こることです。右のボタンを言うと:

enter image description here

下のビューが「ハンドル」の上に描画されないようにするにはどうすればよいですか?

私のプロジェクトからのいくつかの関連する擬似コードが続きます。

私のカスタムビューMyHandleViewは次のように描画します。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Path p = mPath;
    int handleWidth = mHandleWidth;
    int handleHeight = mHandleHeight;
    int left = (getWidth() >> 1) - handleWidth;

    canvas.save();

    // allow drawing out of bounds vertically
    Rect clipBounds = canvas.getClipBounds();
    clipBounds.inset(0, -handleHeight);
    canvas.clipRect(clipBounds, Region.Op.REPLACE);

    // translate up to draw the handle
    canvas.translate(left, -handleHeight);

    // draw my background shape
    canvas.drawPath(p, mPaint);

    canvas.restore();
}

レイアウトは次のようなものです(少し簡略化しています)。

<RelativeLayout
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <!-- Main content of the SlidingUpPanel -->
    <fragment
        Android:above=@+id/panel"
        class="com.neosperience.projects.percassi.Android.home.HomeFragment"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:layout_marginTop="?android:attr/actionBarSize"
        tools:layout="@layout/fragment_home" />

    <!-- The Sliding Panel -->
    <LinearLayout
        Android:id="@id/panel"
        Android:layout_width="match_parent"
        Android:layout_height="@dimen/myFixedSize"
        Android:alignParentBottom="true"
        Android:orientation="vertical"
        Android:clipChildren="false">

        <MyHandleView  xmlns:custom="http://schemas.Android.com/apk/res-auto.com/apk/res/Android"
            Android:layout_width="match_parent"
            Android:layout_height="0dp"
            custom:handleHeight="@dimen/card_panel_handle_height"
            custom:handleWidthRatio="@dimen/card_panel_handle_width_ratio"
            custom:handleBackgroundColor="#000"/>

        <TextView
            Android:id="@+id/loyaltyCardPanelTitle"
            Android:layout_width="match_parent"
            Android:layout_height="@dimen/card_panel_height"
            Android:background="#000"
            Android:gravity="center"
            Android:textStyle="bold"
            Android:textSize="24sp"
            Android:textColor="#fff"
            Android:text="My TEXT"/>
    </LinearLayout>

</RelativeLayout>

フラグメントは、LinearLayoutの下部に2つのボタンを含むビューと考えることができます。

外部RelativeLayoutの代わりに、私は実際にライブラリからのビューを使用しています:SlidingUpPanelLayout( https://github.com/umano/AndroidSlidingUpPanel )。しかし、このRelativeLayoutで動作をテストしました:同じこと、つまりライブラリが関連していないことを意味します。

それをFrameLayoutに配置して、クリッピング境界の外側に描画しないようにすることはできません

これは、ボタンをタッチすると自動的に再描画されますが、他のビュー(階層内のどこかにある)が再描画されないという事実と関係があると思われます(これは最適化であり、無効)。

他の「近い」(または)ビューが無効になるたびにカスタムビューを無効にできるようにしたいので、ビューの無効化には何らかのリスナーが必要ですが、私はそれを知りません。

誰か助けてもらえますか?

41
Daniele Segato

パフォーマンスに最適ではない場合でも、自分で解決策を見つけました。

追加するだけです:

Android:clipChildren="false"

relativeLayout(または任意のレイアウト)に。

これには2つの効果があります(さらに、これは私が興味を持っている2つです):-ViewGroupは、彼の子供の描画をクリップしません(明らか)-ViewGroupは、検討時にダーティ領域との交差をチェックしません(無効)どの子供を再描画する

無効化に関するViewコードを掘り下げました。

プロセスは次のようになります。

  1. ビューはそれ自体を無効にし、通常描画する領域(長方形)が再描画される「ダーティな領域」になります
  2. ビューは、その親(何らかの種類のViewGroup)に自分自身を再描画する必要があることを伝えます
  3. 親はルートの親でも同じことをします
  4. 階層内の各親がすべての子に対してループし、ダーティー領域がそれらの一部と交差するかどうかを確認します
  5. それがあれば、それらも再描画します

ステップ4のクリッピングが関係します。ViewGroupは、clipChildrenがtrueの場合にのみ、子のビュー境界をチェックします。つまり、falseに設定すると、いずれかの子が無効になったときに常にすべての子を再描画しますとなります。

したがって、私のビュー階層は次のようになりました。

ViewGroup A
|
|----- fragment subtree (containing buttons, map,
|       whatever element that I don't want to draw
|       on top of my handle)
|
|----- ViewGroup B
       |
       |---- my handle (which draw outside its clip bounds)

私の場合、「ハンドル」は、通常、フラグメントサブツリーのいくつかの要素によって描画されるものの上にバインドされています。

フラグメント内のビューが無効化されると、ビューツリーの「ダーティリージョン」を渡し、各ビューグループは、そのリージョンに再描画する他の子があるかどうかをチェックします。

ViewGroup Bは、clipBounds = "false"を設定しない場合、クリップ境界の外側に描画したものをクリップします。

フラグメントサブツリーで何かが無効になった場合、ViewGroup Aは、ViewGroup Bのダーティリージョンがフラグメントサブツリーリージョンと交差していないことを確認し、ViewGroup Bの再描画をスキップします。

ただし、ViewGroup Aに子をクリップしないように指示すると、ViewGroup Bに無効化コマンドが与えられ、ハンドルが再描画されます。

そのため、解決策は必ず設定することです

Android:clipChildren="false"

ビューの上の階層内にある、その境界から描画される任意のViewGroupで、描画している範囲外のコンテンツの「下」にあるコンテンツ。

これの明らかな副作用は、ViewGroup A内のビューのいずれかを無効化するたびに、無効化領域を含む無効化呼び出しがその中のすべてのビューに転送されることです。

ただし、clipChildren = "true"(デフォルト)のViewGroup内にあるダーティ領域と交差しないビューはスキップされます。

そのため、これを行う際のパフォーマンスの問題を回避するために、clipChildren = "true"を持つビューグループに多くの "標準"直接子が含まれないようにしてください。 「標準」とは、ビューの境界の外側に描画しないことを意味します。

たとえば、私の例で、ViewGroup Bに多くのビューが含まれる場合、clipChildren = "true"を使用してViewGroup内のすべてをラップし、ViewGroup Bの直接の子としてこのビューグループとその領域の外側を描画する1つのビューのみを残すことを検討してください。 ViewGroup Aの場合.

この単純な事実により、他のビューが無効化されたダーティ領域にない場合、再描画が取得されなくなり、必要な再描画が最小限に抑えられます。

誰かがそれを持っている場合、私はまだそれ以上の考慮事項を聞いて開いています;)

少し待ってから、これを承認済みの回答としてマークします。

編集:多くのデバイスは、clipChildren = "false"の処理で異なることを行います。カスタムウィジェットのすべての親ビューでclipChildren = "false"を設定する必要があることを発見したそれをカバーすることになっていた別のビューの上部に表示します。たとえば、私のレイアウトには、「ハンドル」を覆うはずのナビゲーションドロワーがありました。 NavigationDrawerレイアウトでclipChildren = "false"を設定しなかった場合、開いた引き出しの前にハンドルがポップアップすることがあります。

EDIT2:カスタムウィジェットの高さは0で、自分の「上」に描画されました。 Nexusデバイスでは問題なく動作しましたが、他の多くのデバイスには、高さ0または幅0のビューの描画を完全にスキップする「最適化」がありました。したがって、バインドされていないコンポーネントを作成する場合は、このことに注意してください。少なくとも1ピクセルの高さ/幅を割り当てる必要があります。

86
Daniele Segato