在Android视图剪辑边界外绘图时:如何防止底层视图在我的自定义视图上绘制?

时间:2021-07-12 07:15:25

I'm wrote a custom Android View that need to draw outside its clipping Bounds.

我写了一个自定义Android视图,需要在其剪切边界之外绘制。

This is what I have:

这就是我所拥有的:

在Android视图剪辑边界外绘图时:如何防止底层视图在我的自定义视图上绘制?

This is what happens when I click a button, say the right button:

当我点击一个按钮,即右键时,会发生这种情况:

在Android视图剪辑边界外绘图时:如何防止底层视图在我的自定义视图上绘制?

How do I prevent the View below to draw on top of my "handle"?

如何防止下面的视图在我的“句柄”上绘制?

Some related pseudo-code from my project follow.

我项目中的一些相关伪代码如下。

My custom view MyHandleView draw like this:

我的自定义视图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();
}

The layout is something like this (I simplified a little):

布局是这样的(我简化了一点):

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

You can think of the fragment as a view containing two button at the bottom, in a LinearLayout.

您可以将片段视为包含底部两个按钮的视图,位于LinearLayout中。

In place of the external RelativeLayout I'm really using a view from a library: SlidingUpPanelLayout (https://github.com/umano/AndroidSlidingUpPanel). But I tested the behavior with this RelativeLayout: same thing, meaning the library is not related.

代替外部RelativeLayout,我实际上正在使用库中的视图:SlidingUpPanelLayout(https://github.com/umano/AndroidSlidingUpPanel)。但我测试了这个RelativeLayout的行为:同样的事情,这意味着库不相关。

I say this just to let you know that I can't just place there a FrameLayout and avoid drawing outside the clipping bounds.

我这样说只是为了让你知道我不能只放置一个FrameLayout并避免在剪切边界之外绘制。

I suspect this has something to do with the fact that when I touch the button it redraw itself but my other view (which is somewhere else in the hierarchy) doesn't get re-drawed (this is an optimization and I doubt it can be disabled).

我怀疑这与以下事实有关:当我触摸按钮时它重绘自己,但我的另一个视图(在层次结构中的其他位置)不会重新绘制(这是一个优化,我怀疑它可以是禁用)。

I'd like to be able to invalidate my custom view whenever any other "near" (or any) view get's invalidated, thus I need some kind of listener on view invalidation but I'm not aware of any.

我希望能够在任何其他“近”(或任何)视图失效时使我的自定义视图无效,因此我需要一些关于视图失效的监听器,但我不知道任何。

Can someone help?

有人可以帮忙吗?

1 个解决方案

#1


73  

I found the solution myself, even if this is not optimal for performances.

我自己找到了解决方案,即使这不是表演的最佳选择。

Just add:

只需添加:

android:clipChildren="false"

to the RelativeLayout (or whatever layout you have).

到RelativeLayout(或你有的任何布局)。

This has 2 effects (may be more, this are the two that interested me): - the ViewGroup doesn't clip the drawing of his children (obvious) - the ViewGroup doesn't check for intersection with dirty regions (invalidated) when considering which children to redraw

这有2个效果(可能更多,这是我感兴趣的两个): - ViewGroup不剪切他的孩子的绘图(显而易见) - ViewGroup在考虑时不检查与脏区域的交集(无效)哪些孩子要重绘

I digged the View code about invalidating.

我挖掘了有关无效的View代码。

The process goes, more or like, like this:

这个过程更像或更喜欢这样:

  1. a View invalidate itself, the region it usually draw (a rectangular) become a "dirty region" to be redrawed
  2. a视图使自身无效,它通常绘制的区域(矩形)成为要重绘的“脏区域”
  3. the View tell its parent (a ViewGroup of some kind) it need to redraw itself
  4. View告诉它的父(某种ViewGroup)它需要重绘自己
  5. the parents do the same with it's parent to the root
  6. 父母对其根的父母做同样的事
  7. each parent in the hierarchy loop for every children and check if the dirty region intersect some of them
  8. 层次结构中的每个父项为每个子项循环,并检查脏区域是否与其中一些相交
  9. if it does it also redraw them
  10. 如果它这样做也重绘它们

In step 4 clipping is involved: the ViewGroup check view bounds of his child only if clipChildren is true: meaning that if you place it to false it always redraw all its children when any of them is invalidated.

在步骤4中涉及剪切:仅当clipChildren为true时,ViewGroup检查其子项的视图边界:意味着如果将其置于false,则当其中任何子项无效时,它总是重绘其所有子项。

So, my View hierarchy was like this:

所以,我的View层次结构是这样的:

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)

In my case the "handle" draw ouf of it's bound, on top of something that is usually drawed by some element of the fragment subtree.

在我的情况下,“句柄”在它通常由片段子树的某个元素绘制的东西之上绘制它的束缚。

When any view inside the fragment is invalidated it pass its "dirty region" up in the view tree and each view group check if there are some other children to be redraw in that region.

当片段内的任何视图无效时,它会在视图树中向上传递其“脏区域”,并且每个视图组检查是否有其他子项要在该区域中重绘。

ViewGroup B would clip what I draw outside the clip bounds if I do not set clipBounds="false" on it.

如果我没有在其上设置clipBounds =“false”,ViewGroup B会剪切我在剪辑边界外绘制的内容。

If anything get's invalidated in the fragment subtree the ViewGroup A will see that ViewGroup B dirty region is not intersecting the fragment subtree region and will skip redrawing of ViewGroup B.

如果片段子树中的任何内容无效,则ViewGroup A将看到ViewGroup B脏区域未与片段子树区域相交,并将跳过重绘ViewGroup B.

But if I also tell ViewGroup A to not clip children it will still give ViewGroup B an invalidate command which will then cause a redraw of my handle.

但是,如果我还告诉ViewGroup A不剪辑子节点,它仍然会给ViewGroup B一个无效命令,这将导致重新绘制我的句柄。

So the solution is to make sure to set

所以解决方案是确保设置

android:clipChildren="false"

on any ViewGroup in the hierarchy above the View that draw out of it's bounds on which the content may fall "under" the out-of-bound region you are drawing.

在视图上方的层次结构中的任何ViewGroup上绘制出内容可能落在您正在绘制的越界区域“下方”的边界。

The obvious side effect of this is that whenever I invalidate any of the view inside ViewGroup A an invalidate call will be forwarded, with the invalid region, to all the view in it.

显而易见的副作用是,每当我使ViewGroup A中的任何视图无效时,无效区域将被转发到其中的所有视图。

However any view that doesn't intersect the dirty region which is inside a ViewGroup with clipChildren="true" (default) will be skipped.

但是,将跳过任何不与ViewGroup内的脏区域相交的视图,其中clipChildren =“true”(默认值)。

So to avoid performance issues when doing this make sure your view groups with clipChildren="true" have not many "standard" direct children. And with "standard" I mean that they do not draw outside their view bounds.

因此,为避免性能问题,请确保使用clipChildren =“true”的视图组没有很多“标准”直接子项。而对于“标准”,我的意思是他们不会超出他们的视角。

So for example if in my example ViewGroup B contains many view consider wrapping all those in a ViewGroup with clipChildren="true" and only leave this view group and the one view that draw outside its region as direct children of ViewGroup B. The same goes for ViewGroup A.

因此,例如,如果在我的示例中,ViewGroup B包含许多视图,请考虑使用clipChildren =“true”将所有这些视图包装在ViewGroup中,并且只保留此视图组以及在其区域之外绘制的一个视图作为ViewGroup B的直接子项。同样如此对于ViewGroup A.

This simple fact will make sure no other View will get a redraw if they aren't in the invalidated dirty region minimizing the redraws needed.

这个简单的事实将确保没有其他View将重绘,如果它们不在无效的脏区域中,最小化所需的重绘。

I'm still open to hear any more consideration if someone has one ;)

如果有人有话,我仍然愿意再听一次;)

So I'll wait a little bit before marking this as accepted answer.

所以我会等一下,然后将其标记为已接受的答案。

EDIT: Many devices do something different in handling clipChildren="false". I discovered that I had to set clipChildren="false" on all the parent views of my custom widget that may contains elements in their hierarchy which should draw over of the "out of bound region" of the widget or you may see your custom drawing showing ON TOP of another view that was supposed to cover it. For example in my layout I had a Navigation Drawer that was supposed to cover my "handle". If I didn't set clipChildren="false" on the NavigationDrawer layout I may sometimes see my handle pop up in front of the opened drawer.

编辑:许多设备在处理clipChildren =“false”时做了一些不同的事情。我发现我必须在我的自定义窗口小部件的所有父视图上设置clipChildren =“false”,这些视图可能包含其层次结构中应该覆盖窗口小部件的“越界区域”的元素,或者您可能会看到自定义绘图显示应该覆盖它的另一个视图的TOP。例如,在我的布局中,我有一个导航抽屉,应该覆盖我的“手柄”。如果我没有在NavigationDrawer布局上设置clipChildren =“false”,我有时会看到我的手柄弹出打开的抽屉前面。

EDIT2: My custom widget had 0 height and drawed "on top" of itself. Worked fine on Nexus devices but many of the others had some "optimization" in place that completely skip drawing of views that have 0 height or 0 width. So be aware of this if you want to write a component that draw out of it's bound: you have to assign it at least 1 pixel height / width.

编辑2:我的自定义小部件有0高度,并在自己的“顶部”绘制。在Nexus设备上工作得很好,但其他许多设备都有一些“优化”,完全跳过了高度为0或宽度为0的视图。因此,如果你想编写一个从它的边界绘制的组件,请注意这一点:你必须为它分配至少1个像素的高度/宽度。

#1


73  

I found the solution myself, even if this is not optimal for performances.

我自己找到了解决方案,即使这不是表演的最佳选择。

Just add:

只需添加:

android:clipChildren="false"

to the RelativeLayout (or whatever layout you have).

到RelativeLayout(或你有的任何布局)。

This has 2 effects (may be more, this are the two that interested me): - the ViewGroup doesn't clip the drawing of his children (obvious) - the ViewGroup doesn't check for intersection with dirty regions (invalidated) when considering which children to redraw

这有2个效果(可能更多,这是我感兴趣的两个): - ViewGroup不剪切他的孩子的绘图(显而易见) - ViewGroup在考虑时不检查与脏区域的交集(无效)哪些孩子要重绘

I digged the View code about invalidating.

我挖掘了有关无效的View代码。

The process goes, more or like, like this:

这个过程更像或更喜欢这样:

  1. a View invalidate itself, the region it usually draw (a rectangular) become a "dirty region" to be redrawed
  2. a视图使自身无效,它通常绘制的区域(矩形)成为要重绘的“脏区域”
  3. the View tell its parent (a ViewGroup of some kind) it need to redraw itself
  4. View告诉它的父(某种ViewGroup)它需要重绘自己
  5. the parents do the same with it's parent to the root
  6. 父母对其根的父母做同样的事
  7. each parent in the hierarchy loop for every children and check if the dirty region intersect some of them
  8. 层次结构中的每个父项为每个子项循环,并检查脏区域是否与其中一些相交
  9. if it does it also redraw them
  10. 如果它这样做也重绘它们

In step 4 clipping is involved: the ViewGroup check view bounds of his child only if clipChildren is true: meaning that if you place it to false it always redraw all its children when any of them is invalidated.

在步骤4中涉及剪切:仅当clipChildren为true时,ViewGroup检查其子项的视图边界:意味着如果将其置于false,则当其中任何子项无效时,它总是重绘其所有子项。

So, my View hierarchy was like this:

所以,我的View层次结构是这样的:

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)

In my case the "handle" draw ouf of it's bound, on top of something that is usually drawed by some element of the fragment subtree.

在我的情况下,“句柄”在它通常由片段子树的某个元素绘制的东西之上绘制它的束缚。

When any view inside the fragment is invalidated it pass its "dirty region" up in the view tree and each view group check if there are some other children to be redraw in that region.

当片段内的任何视图无效时,它会在视图树中向上传递其“脏区域”,并且每个视图组检查是否有其他子项要在该区域中重绘。

ViewGroup B would clip what I draw outside the clip bounds if I do not set clipBounds="false" on it.

如果我没有在其上设置clipBounds =“false”,ViewGroup B会剪切我在剪辑边界外绘制的内容。

If anything get's invalidated in the fragment subtree the ViewGroup A will see that ViewGroup B dirty region is not intersecting the fragment subtree region and will skip redrawing of ViewGroup B.

如果片段子树中的任何内容无效,则ViewGroup A将看到ViewGroup B脏区域未与片段子树区域相交,并将跳过重绘ViewGroup B.

But if I also tell ViewGroup A to not clip children it will still give ViewGroup B an invalidate command which will then cause a redraw of my handle.

但是,如果我还告诉ViewGroup A不剪辑子节点,它仍然会给ViewGroup B一个无效命令,这将导致重新绘制我的句柄。

So the solution is to make sure to set

所以解决方案是确保设置

android:clipChildren="false"

on any ViewGroup in the hierarchy above the View that draw out of it's bounds on which the content may fall "under" the out-of-bound region you are drawing.

在视图上方的层次结构中的任何ViewGroup上绘制出内容可能落在您正在绘制的越界区域“下方”的边界。

The obvious side effect of this is that whenever I invalidate any of the view inside ViewGroup A an invalidate call will be forwarded, with the invalid region, to all the view in it.

显而易见的副作用是,每当我使ViewGroup A中的任何视图无效时,无效区域将被转发到其中的所有视图。

However any view that doesn't intersect the dirty region which is inside a ViewGroup with clipChildren="true" (default) will be skipped.

但是,将跳过任何不与ViewGroup内的脏区域相交的视图,其中clipChildren =“true”(默认值)。

So to avoid performance issues when doing this make sure your view groups with clipChildren="true" have not many "standard" direct children. And with "standard" I mean that they do not draw outside their view bounds.

因此,为避免性能问题,请确保使用clipChildren =“true”的视图组没有很多“标准”直接子项。而对于“标准”,我的意思是他们不会超出他们的视角。

So for example if in my example ViewGroup B contains many view consider wrapping all those in a ViewGroup with clipChildren="true" and only leave this view group and the one view that draw outside its region as direct children of ViewGroup B. The same goes for ViewGroup A.

因此,例如,如果在我的示例中,ViewGroup B包含许多视图,请考虑使用clipChildren =“true”将所有这些视图包装在ViewGroup中,并且只保留此视图组以及在其区域之外绘制的一个视图作为ViewGroup B的直接子项。同样如此对于ViewGroup A.

This simple fact will make sure no other View will get a redraw if they aren't in the invalidated dirty region minimizing the redraws needed.

这个简单的事实将确保没有其他View将重绘,如果它们不在无效的脏区域中,最小化所需的重绘。

I'm still open to hear any more consideration if someone has one ;)

如果有人有话,我仍然愿意再听一次;)

So I'll wait a little bit before marking this as accepted answer.

所以我会等一下,然后将其标记为已接受的答案。

EDIT: Many devices do something different in handling clipChildren="false". I discovered that I had to set clipChildren="false" on all the parent views of my custom widget that may contains elements in their hierarchy which should draw over of the "out of bound region" of the widget or you may see your custom drawing showing ON TOP of another view that was supposed to cover it. For example in my layout I had a Navigation Drawer that was supposed to cover my "handle". If I didn't set clipChildren="false" on the NavigationDrawer layout I may sometimes see my handle pop up in front of the opened drawer.

编辑:许多设备在处理clipChildren =“false”时做了一些不同的事情。我发现我必须在我的自定义窗口小部件的所有父视图上设置clipChildren =“false”,这些视图可能包含其层次结构中应该覆盖窗口小部件的“越界区域”的元素,或者您可能会看到自定义绘图显示应该覆盖它的另一个视图的TOP。例如,在我的布局中,我有一个导航抽屉,应该覆盖我的“手柄”。如果我没有在NavigationDrawer布局上设置clipChildren =“false”,我有时会看到我的手柄弹出打开的抽屉前面。

EDIT2: My custom widget had 0 height and drawed "on top" of itself. Worked fine on Nexus devices but many of the others had some "optimization" in place that completely skip drawing of views that have 0 height or 0 width. So be aware of this if you want to write a component that draw out of it's bound: you have to assign it at least 1 pixel height / width.

编辑2:我的自定义小部件有0高度,并在自己的“顶部”绘制。在Nexus设备上工作得很好,但其他许多设备都有一些“优化”,完全跳过了高度为0或宽度为0的视图。因此,如果你想编写一个从它的边界绘制的组件,请注意这一点:你必须为它分配至少1个像素的高度/宽度。