Skip to content

Commit a352178

Browse files
drchenhunterstich
authored andcommitted
[TextField] Fix label cutout doesn't work on API < 18
Android framework Canvas.clipRect() has a bug with Region.Op.DIFFERENCE when handling bounds.left on APIs lower than 18, which causes text field outlines are still drawn over the label on lower APIs, despite the label area is supposed to be cut out. Fixes this by bringing back the old solution we were using - "painting out" the label area after the stroke is drawn. Since the implementation is quite complicated, this CL also splits the CutoutDrawable to two inner impl classes to have a better code structure. Resolves #2811 (comment) PiperOrigin-RevId: 482013070
1 parent bdb8253 commit a352178

File tree

2 files changed

+101
-16
lines changed

2 files changed

+101
-16
lines changed

lib/java/com/google/android/material/textfield/CutoutDrawable.java

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717
package com.google.android.material.textfield;
1818

19+
import android.annotation.TargetApi;
1920
import android.graphics.Canvas;
21+
import android.graphics.Color;
22+
import android.graphics.Paint;
23+
import android.graphics.Paint.Style;
24+
import android.graphics.PorterDuff.Mode;
25+
import android.graphics.PorterDuffXfermode;
2026
import android.graphics.RectF;
2127
import android.graphics.Region.Op;
2228
import android.os.Build.VERSION;
2329
import android.os.Build.VERSION_CODES;
30+
import android.view.View;
2431
import androidx.annotation.NonNull;
2532
import androidx.annotation.Nullable;
2633
import com.google.android.material.shape.MaterialShapeDrawable;
@@ -31,13 +38,15 @@
3138
* outline mode.
3239
*/
3340
class CutoutDrawable extends MaterialShapeDrawable {
34-
@NonNull private final RectF cutoutBounds;
41+
@NonNull protected final RectF cutoutBounds;
3542

36-
CutoutDrawable() {
37-
this(null);
43+
static CutoutDrawable create(@Nullable ShapeAppearanceModel shapeAppearanceModel) {
44+
return VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2
45+
? new ImplApi18(shapeAppearanceModel)
46+
: new ImplApi14(shapeAppearanceModel);
3847
}
3948

40-
CutoutDrawable(@Nullable ShapeAppearanceModel shapeAppearanceModel) {
49+
private CutoutDrawable(@Nullable ShapeAppearanceModel shapeAppearanceModel) {
4150
super(shapeAppearanceModel != null ? shapeAppearanceModel : new ShapeAppearanceModel());
4251
cutoutBounds = new RectF();
4352
}
@@ -67,20 +76,96 @@ void removeCutout() {
6776
setCutout(0, 0, 0, 0);
6877
}
6978

70-
@Override
71-
protected void drawStrokeShape(@NonNull Canvas canvas) {
72-
if (cutoutBounds.isEmpty()) {
73-
super.drawStrokeShape(canvas);
74-
} else {
75-
// Saves the canvas so we can restore the clip after drawing the stroke.
76-
canvas.save();
77-
if (VERSION.SDK_INT >= VERSION_CODES.O) {
78-
canvas.clipOutRect(cutoutBounds);
79+
@TargetApi(VERSION_CODES.JELLY_BEAN_MR2)
80+
private static class ImplApi18 extends CutoutDrawable {
81+
ImplApi18(@Nullable ShapeAppearanceModel shapeAppearanceModel) {
82+
super(shapeAppearanceModel);
83+
}
84+
85+
@Override
86+
protected void drawStrokeShape(@NonNull Canvas canvas) {
87+
if (cutoutBounds.isEmpty()) {
88+
super.drawStrokeShape(canvas);
7989
} else {
80-
canvas.clipRect(cutoutBounds, Op.DIFFERENCE);
90+
// Saves the canvas so we can restore the clip after drawing the stroke.
91+
canvas.save();
92+
if (VERSION.SDK_INT >= VERSION_CODES.O) {
93+
canvas.clipOutRect(cutoutBounds);
94+
} else {
95+
canvas.clipRect(cutoutBounds, Op.DIFFERENCE);
96+
}
97+
super.drawStrokeShape(canvas);
98+
canvas.restore();
8199
}
100+
}
101+
}
102+
103+
// Workaround: Canvas.clipRect() had a bug before API 18 - bound.left didn't work correctly
104+
// with Region.Op.DIFFERENCE. "Paints out" the cutout area instead on lower APIs.
105+
private static class ImplApi14 extends CutoutDrawable {
106+
private Paint cutoutPaint;
107+
private int savedLayer;
108+
109+
ImplApi14(@Nullable ShapeAppearanceModel shapeAppearanceModel) {
110+
super(shapeAppearanceModel);
111+
}
112+
113+
@Override
114+
public void draw(@NonNull Canvas canvas) {
115+
preDraw(canvas);
116+
super.draw(canvas);
117+
postDraw(canvas);
118+
}
119+
120+
@Override
121+
protected void drawStrokeShape(@NonNull Canvas canvas) {
82122
super.drawStrokeShape(canvas);
83-
canvas.restore();
123+
canvas.drawRect(cutoutBounds, getCutoutPaint());
124+
}
125+
126+
private Paint getCutoutPaint() {
127+
if (cutoutPaint == null) {
128+
cutoutPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
129+
cutoutPaint.setStyle(Style.FILL_AND_STROKE);
130+
cutoutPaint.setColor(Color.WHITE);
131+
cutoutPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT));
132+
}
133+
return cutoutPaint;
134+
}
135+
136+
private void preDraw(@NonNull Canvas canvas) {
137+
Callback callback = getCallback();
138+
139+
if (useHardwareLayer(callback)) {
140+
View viewCallback = (View) callback;
141+
// Make sure we're using a hardware layer.
142+
if (viewCallback.getLayerType() != View.LAYER_TYPE_HARDWARE) {
143+
viewCallback.setLayerType(View.LAYER_TYPE_HARDWARE, null);
144+
}
145+
} else {
146+
// If we're not using a hardware layer, save the canvas layer.
147+
saveCanvasLayer(canvas);
148+
}
149+
}
150+
151+
private void saveCanvasLayer(@NonNull Canvas canvas) {
152+
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
153+
savedLayer = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null);
154+
} else {
155+
savedLayer =
156+
canvas.saveLayer(
157+
0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
158+
}
159+
}
160+
161+
private void postDraw(@NonNull Canvas canvas) {
162+
if (!useHardwareLayer(getCallback())) {
163+
canvas.restoreToCount(savedLayer);
164+
}
165+
}
166+
167+
private boolean useHardwareLayer(Callback callback) {
168+
return callback instanceof View;
84169
}
85170
}
86171
}

lib/java/com/google/android/material/textfield/TextInputLayout.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ private void assignBoxBackgroundByMode() {
785785
break;
786786
case BOX_BACKGROUND_OUTLINE:
787787
if (hintEnabled && !(boxBackground instanceof CutoutDrawable)) {
788-
boxBackground = new CutoutDrawable(shapeAppearanceModel);
788+
boxBackground = CutoutDrawable.create(shapeAppearanceModel);
789789
} else {
790790
boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
791791
}

0 commit comments

Comments
 (0)