|
16 | 16 |
|
17 | 17 | package com.google.android.material.textfield;
|
18 | 18 |
|
| 19 | +import android.annotation.TargetApi; |
19 | 20 | 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; |
20 | 26 | import android.graphics.RectF;
|
21 | 27 | import android.graphics.Region.Op;
|
22 | 28 | import android.os.Build.VERSION;
|
23 | 29 | import android.os.Build.VERSION_CODES;
|
| 30 | +import android.view.View; |
24 | 31 | import androidx.annotation.NonNull;
|
25 | 32 | import androidx.annotation.Nullable;
|
26 | 33 | import com.google.android.material.shape.MaterialShapeDrawable;
|
|
31 | 38 | * outline mode.
|
32 | 39 | */
|
33 | 40 | class CutoutDrawable extends MaterialShapeDrawable {
|
34 |
| - @NonNull private final RectF cutoutBounds; |
| 41 | + @NonNull protected final RectF cutoutBounds; |
35 | 42 |
|
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); |
38 | 47 | }
|
39 | 48 |
|
40 |
| - CutoutDrawable(@Nullable ShapeAppearanceModel shapeAppearanceModel) { |
| 49 | + private CutoutDrawable(@Nullable ShapeAppearanceModel shapeAppearanceModel) { |
41 | 50 | super(shapeAppearanceModel != null ? shapeAppearanceModel : new ShapeAppearanceModel());
|
42 | 51 | cutoutBounds = new RectF();
|
43 | 52 | }
|
@@ -67,20 +76,96 @@ void removeCutout() {
|
67 | 76 | setCutout(0, 0, 0, 0);
|
68 | 77 | }
|
69 | 78 |
|
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); |
79 | 89 | } 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(); |
81 | 99 | }
|
| 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) { |
82 | 122 | 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; |
84 | 169 | }
|
85 | 170 | }
|
86 | 171 | }
|
0 commit comments