Android View Clipping

Android: how to clip only top rounded corners

I've managed to get this working by creating a custom ViewOutlineProvider and using that instead of a background value

ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(final View view, final Outline outline) {
float cornerRadiusDP = 16f;
float cornerRadius = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, cornerRadiusDP, getResources().getDisplayMetrics());
outline.setRoundRect(0, 0, view.getWidth(), (int)(view.getHeight() + cornerRadius), cornerRadius);
}
};
scrollView.setOutlineProvider(mViewOutlineProvider);
scrollView.setClipToOutline(true);

Android make view fill it's background shape

Yes, it's possible to do this. It's called View Clipping.

Here is an existing question about clipping:
Android View Clipping

You have 2 options:

1- Implement a custom View and override onDraw so you can perform the clipping manually. See here: https://stackoverflow.com/a/28206555/6007104

2- Use View.outlineProvider and View.clipToOutline. This is the easier option, but only available on SDK 21+. See here: https://stackoverflow.com/a/54202660/6007104

When drawing outside the view clip bounds with Android: how do I prevent underling views from drawing on top of my custom view?

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

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

I digged the View code about invalidating.

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. the View tell its parent (a ViewGroup of some kind) it need to redraw itself
  3. the parents do the same with it's parent to the root
  4. each parent in the hierarchy loop for every children and check if the dirty region intersect some of them
  5. if it does it also redraw them

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.

So, my View hierarchy was like this:

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.

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.

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.

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.

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.

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

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.

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.

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.

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.

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.

Do not clip bounds of AndroidView in Compose

It's a known feature request, here's a workaround until it's implemented:

@Composable
fun <T : View> AndroidView(
clipToBounds: Boolean,
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate,
) {
androidx.compose.ui.viewinterop.AndroidView(
factory = factory,
modifier = modifier,
update = if (clipToBounds) {
update
} else {
{
(it.parent as? ViewGroup)?.clipChildren = false
update(it)
}
}
)
}

Enable clip on single child of clipsChildren=false parent

You will have to do your own clipping on the view that should be clipped. For the ConstraintLayout set android:clipChildren="false".

Write a custom view that extends the type of view that you want to clip. Change the draw() method to something like the following to clip any part of the view that falls outside of the parent.

In the image below, the four corners of the large red square are occupied with text views that are clipped. You can see the outline of the full text views in the following image. The top center purple square is child that is not clipped.

Sample Image

Here is the custom view that is used:

class ClippedChild @JvmOverloads constructor(  
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {
override fun draw(canvas: Canvas) {
val parent = parent as ViewGroup
val clippingRect = Rect()
getDrawingRect(clippingRect)

if (left < 0) {
clippingRect.left = -left
}
if (top < 0) {
clippingRect.top = -top
}
if (right > parent.width) {
clippingRect.right = width - (right - parent.width)
}
if (bottom > parent.height) {
clippingRect.bottom = height - (bottom - parent.height)
}
canvas.save()
canvas.clipRect(clippingRect)
super.draw(canvas)
canvas.restore()
}
}

...and the XML for the layout:

 <androidx.constraintlayout.widget.ConstraintLayout 
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
tools:context=".MainActivity">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/innerLayout"
android:layout_width="250dp"
android:layout_height="250dp"
android:background="#F44336"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<View
android:id="@+id/ViewA"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9C27B0"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.example.myapplication.ClippedChild
android:id="@+id/ViewB1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#9C27B0"
android:text="top\n/start"
android:gravity="bottom|end"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="parent" />

<com.example.myapplication.ClippedChild
android:id="@+id/ViewB2"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="top\n/end"
android:gravity="bottom|start"
android:background="#2196F3"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

<com.example.myapplication.ClippedChild
android:id="@+id/ViewB3"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#8BC34A"
android:text="bottom\n/end"
android:gravity="top|start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toBottomOf="parent" />

<com.example.myapplication.ClippedChild
android:id="@+id/ViewB4"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FFEB3B"
android:text="bottom\n/start"
android:gravity="top|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent" />

<com.example.myapplication.ClippedChild
android:id="@+id/ViewB5"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FFEB3B"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>


Related Topics



Leave a reply



Submit