web-dev-qa-db-fra.com

Lorsque je dessine en dehors des limites du clip de la vue avec Android: comment empêcher les sous-vues de dessiner au-dessus de ma vue personnalisée?

J'ai écrit une coutume Android Voir qui doit dessiner en dehors de ses limites de découpage.

C'est ce que j'ai

enter image description here

Voici ce qui se passe lorsque je clique sur un bouton, dis le bon bouton

enter image description here

Comment puis-je empêcher la vue ci-dessous de dessiner au-dessus de ma "poignée"?

Un pseudo-code lié à mon projet suit.

Ma vue personnalisée MyHandleView dessine comme ceci:

@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();
}

La disposition ressemble à ceci (j'ai simplifié un peu):

<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>

Vous pouvez considérer le fragment comme une vue contenant deux boutons en bas, dans LinearLayout.

Au lieu de RelativeLayout externe, j'utilise vraiment une vue d'une bibliothèque: SlidingUpPanelLayout ( https://github.com/umano/AndroidSlidingUpPanel ). Mais j'ai testé le comportement avec RelativeLayout: même chose, ce qui signifie que la bibliothèque n'est pas liée.

Je dis cela simplement pour vous faire savoir que je ne peux pas simplement y placer un FrameLayout et éviter de dessiner en dehors des limites de l'écrêtage .

Je soupçonne que cela a quelque chose à voir avec le fait que lorsque je touche le bouton, il se redessine mais mon autre vue (qui se trouve quelque part dans la hiérarchie) ne se redessine pas (ceci est une optimisation et je doute que cela puisse être désactivée).

J'aimerais pouvoir invalider ma vue personnalisée chaque fois que toute autre vue "proche" (ou toute vue) est invalidée; j'ai donc besoin d'une sorte d'auditeur pour l'invalidation de la vue, mais je n'en connais aucune.

Quelqu'un peut aider?

41
Daniele Segato

J'ai trouvé la solution moi-même, même si ce n'est pas optimal pour les performances.

Ajoutez simplement:

Android:clipChildren="false"

vers le RelativeLayout (ou la mise en page que vous avez).

Cela a 2 effets (peut-être plus, ce sont les deux qui m'ont intéressé): - le ViewGroup ne coupe pas le dessin de ses enfants (évident) - le ViewGroup ne vérifie pas l'intersection avec des régions sales (invalidé) lors de l'examen quels enfants redessiner

J'ai creusé le code View sur l'invalidation.

Le processus va, plus ou comme, comme ceci:

  1. une vue s'invalide, la région qu'il dessine (un rectangle) devient une "région sale" à redessiner
  2. la vue indique à son parent (un ViewGroup) qu'il doit se redessiner
  3. les parents font la même chose avec ses parents à la racine
  4. chaque parent dans la boucle hiérarchique pour chaque enfant et vérifiez si la région sale croise certains d'entre eux
  5. si ça le fait aussi les redessiner

À l'étape 4, le découpage est impliqué: la vue ViewGroup contrôle les limites de la vue de son enfant uniquement si clipChildren est true: cela signifie que si vous le placez sur false, redessine toujours tous ses enfants lorsque l'un d'entre eux est invalidé.

Donc, ma hiérarchie de vues était comme ceci:

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)

Dans mon cas, le "handle" dessine sa partie liée, en plus de quelque chose qui est habituellement dessiné par un élément du sous-arbre fragment.

Quand une vue à l'intérieur du fragment est invalidée, elle passe sa "région sale" dans l'arborescence de la vue et chaque groupe de vues vérifie s'il y a d'autres enfants à redessiner dans cette région.

ViewGroup B découperait ce que je dessinais en dehors des limites du clip si je ne définissais pas clipBounds = "false" dessus.

Si quelque chose est invalidé dans la sous-arborescence de fragments, le ViewGroup A verra que la région sale du ViewGroup B ne coupe pas la région de la sous-arborescence de fragments et ignorera la nouvelle mise à jour de ViewGroup B.

Mais si je dis aussi à ViewGroup A de ne pas couper les enfants, il donnera toujours à ViewGroup B une commande d'invalidation qui provoquera ensuite un rafraîchissement de mon descripteur.

Donc, la solution est de s'assurer de définir

Android:clipChildren="false"

sur tout groupe de vues de la hiérarchie située au-dessus de la vue et sortant de ses limites sur lesquelles le contenu peut tomber "sous" la région hors limites que vous dessinez.

L’effet secondaire évident de cette situation est que chaque fois que j’invalide une vue dans ViewGroup A, un appel invalide est renvoyé, avec la région non valide, à toutes les vues qu’il contient.

Cependant, toute vue qui ne croise pas la région sale qui se trouve dans un ViewGroup avec clipChildren = "true" (par défaut) sera ignorée.

Par conséquent, pour éviter les problèmes de performances, assurez-vous que vos groupes de vues avec clipChildren = "true" n'ont pas beaucoup d'enfants directs "standard". Et par "standard", je veux dire qu’ils ne tirent pas en dehors de leurs limites de vue.

Ainsi, par exemple, si dans mon exemple, ViewGroup B contient de nombreuses vues, envisagez de les envelopper dans un ViewGroup avec clipChildren = "true" et de ne laisser que ce groupe de vues et la vue qui tire en dehors de sa région en tant qu'enfants directs de ViewGroup B. pour ViewGroup A.

Ce simple fait permettra de s'assurer qu'aucune autre vue ne sera redessinée si elle ne se trouve pas dans la région sale invalidée, ce qui minimise les redessinages nécessaires.

Je suis toujours ouvert pour entendre plus de considération si quelqu'un en a un;)

Je vais donc attendre un peu avant de marquer ceci comme une réponse acceptée.

EDIT: De nombreux périphériques font quelque chose de différent en manipulant clipChildren = "false". J'ai découvert que je devais définir clipChildren = "false" sur toutes les vues parentes de mon widget personnalisé pouvant contenir des éléments dans leur hiérarchie devant dessiner la "zone hors limites" du widget, sinon votre dessin personnalisé pourrait s'afficher. montrant en haut d’une autre vue censée la couvrir. Par exemple, dans ma mise en page, j'avais un tiroir de navigation qui était censé couvrir ma "poignée". Si je n'ai pas défini clipChildren = "false" dans la présentation NavigationDrawer, je peux parfois voir ma poignée apparaître devant le tiroir ouvert.

EDIT2: Mon widget personnalisé avait une hauteur de 0 et était dessiné "par dessus" de lui-même. Fonctionnait correctement sur les appareils Nexus, mais beaucoup d’autres avaient mis en place une "optimisation" qui ignorait complètement le dessin de vues ayant une hauteur ou une largeur égales à 0. Soyez donc conscient de cela si vous voulez écrire un composant qui tire hors de sa limite: vous devez lui attribuer au moins 1 pixel hauteur/largeur.

86
Daniele Segato