blob: 61673feb7e24613ddde75e0ced0ac117d4f4d1f5 [file] [log] [blame]
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -08001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.core.graphics;
18
Nader Jawad39bec902019-06-24 16:10:48 -070019import static androidx.core.graphics.BlendModeUtils.obtainBlendModeFromCompat;
20import static androidx.core.graphics.BlendModeUtils.obtainPorterDuffFromCompat;
21
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080022import android.graphics.Paint;
Nader Jawadee1f6f12019-03-12 15:11:17 -070023import android.graphics.PorterDuff;
Nader Jawadee1f6f12019-03-12 15:11:17 -070024import android.graphics.PorterDuffXfermode;
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080025import android.graphics.Rect;
26import android.os.Build;
Aurimas Liutikas9dede512018-03-13 14:08:08 -070027
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080028import androidx.annotation.NonNull;
Nader Jawadee1f6f12019-03-12 15:11:17 -070029import androidx.annotation.Nullable;
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080030import androidx.core.util.Pair;
31
32/**
33 * Helper for accessing features in {@link Paint}.
34 */
35public final class PaintCompat {
36 // U+DFFFD which is very end of unassigned plane.
37 private static final String TOFU_STRING = "\uDB3F\uDFFD";
38 private static final String EM_STRING = "m";
39
40 private static final ThreadLocal<Pair<Rect, Rect>> sRectThreadLocal = new ThreadLocal<>();
41
42 /**
43 * Determine whether the typeface set on the paint has a glyph supporting the
44 * string in a backwards compatible way.
45 *
46 * @param paint the paint instance to check
47 * @param string the string to test whether there is glyph support
48 * @return true if the typeface set on the given paint has a glyph for the string
49 */
50 public static boolean hasGlyph(@NonNull Paint paint, @NonNull String string) {
51 if (Build.VERSION.SDK_INT >= 23) {
Alan Viverette7ad7d9092022-01-13 15:21:21 +000052 return paint.hasGlyph(string);
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -080053 }
54 final int length = string.length();
55
56 if (length == 1 && Character.isWhitespace(string.charAt(0))) {
57 // measureText + getTextBounds skips whitespace so we need to special case it here
58 return true;
59 }
60
61 final float missingGlyphWidth = paint.measureText(TOFU_STRING);
62 final float emGlyphWidth = paint.measureText(EM_STRING);
63
64 final float width = paint.measureText(string);
65
66 if (width == 0f) {
67 // If the string width is 0, it can't be rendered
68 return false;
69 }
70
71 if (string.codePointCount(0, string.length()) > 1) {
72 // Heuristic to detect fallback glyphs for ligatures like flags and ZWJ sequences
73 // Return false if string is rendered too widely
74 if (width > 2 * emGlyphWidth) {
75 return false;
76 }
77
78 // Heuristic to detect fallback glyphs for ligatures like flags and ZWJ sequences (2).
79 // If width is greater than or equal to the sum of width of each code point, it is very
80 // likely that the system is using fallback fonts to draw {@code string} in two or more
81 // glyphs instead of a single ligature glyph. (hasGlyph returns false in this case.)
82 // False detections are possible (the ligature glyph may happen to have the same width
83 // as the sum width), but there are no good way to avoid them.
84 // NOTE: This heuristic does not work with proportional glyphs.
85 // NOTE: This heuristic does not work when a ZWJ sequence is partially combined.
86 // E.g. If system has a glyph for "A ZWJ B" and not for "A ZWJ B ZWJ C", this heuristic
87 // returns true for "A ZWJ B ZWJ C".
88 float sumWidth = 0;
89 int i = 0;
90 while (i < length) {
91 int charCount = Character.charCount(string.codePointAt(i));
92 sumWidth += paint.measureText(string, i, i + charCount);
93 i += charCount;
94 }
95 if (width >= sumWidth) {
96 return false;
97 }
98 }
99
100 if (width != missingGlyphWidth) {
101 // If the widths are different then its not tofu
102 return true;
103 }
104
105 // If the widths are the same, lets check the bounds. The chance of them being
106 // different chars with the same bounds is extremely small
107 final Pair<Rect, Rect> rects = obtainEmptyRects();
108 paint.getTextBounds(TOFU_STRING, 0, TOFU_STRING.length(), rects.first);
109 paint.getTextBounds(string, 0, length, rects.second);
110 return !rects.first.equals(rects.second);
111 }
112
Nader Jawadee1f6f12019-03-12 15:11:17 -0700113 /**
114 * Configure the corresponding BlendMode on the given paint. If the Android platform supports
115 * the blend mode natively, it will fall back on the framework implementation of either
116 * BlendMode or PorterDuff mode. If it is not supported then this method is a no-op
117 * @param paint target Paint to which the BlendMode will be applied
118 * @param blendMode BlendMode to configure on the paint if it is supported by the platform
119 * version. A value of null removes the BlendMode from the Paint and restores
120 * it to the default
Nader Jawaddb1dcb12019-04-24 15:03:42 -0700121 * @return true if the specified BlendMode as applied successfully, false if the platform
122 * version does not support this BlendMode. If the BlendMode is not supported, this falls
123 * back to the default BlendMode
Nader Jawadee1f6f12019-03-12 15:11:17 -0700124 */
Nader Jawaddb1dcb12019-04-24 15:03:42 -0700125 public static boolean setBlendMode(@NonNull Paint paint, @Nullable BlendModeCompat blendMode) {
Clara Bayarrid20f2c12019-04-29 15:04:04 +0100126 if (Build.VERSION.SDK_INT >= 29) {
Alan Viverette7ad7d9092022-01-13 15:21:21 +0000127 paint.setBlendMode(blendMode != null ? obtainBlendModeFromCompat(blendMode) : null);
Nader Jawaddb1dcb12019-04-24 15:03:42 -0700128 // All blend modes supported in Q
129 return true;
130 } else if (blendMode != null) {
Nader Jawadee1f6f12019-03-12 15:11:17 -0700131 PorterDuff.Mode mode = obtainPorterDuffFromCompat(blendMode);
Nader Jawaddb1dcb12019-04-24 15:03:42 -0700132 paint.setXfermode(mode != null ? new PorterDuffXfermode(mode) : null);
133 // If the BlendMode has an equivalent PorterDuff mode, return true,
134 // otherwise return false
135 return mode != null;
136 } else {
137 // Configuration of a null BlendMode falls back to the default which is supported in
138 // all platform levels
139 paint.setXfermode(null);
140 return true;
Nader Jawadee1f6f12019-03-12 15:11:17 -0700141 }
142 }
143
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -0800144 private static Pair<Rect, Rect> obtainEmptyRects() {
145 Pair<Rect, Rect> rects = sRectThreadLocal.get();
146 if (rects == null) {
147 rects = new Pair<>(new Rect(), new Rect());
148 sRectThreadLocal.set(rects);
149 } else {
150 rects.first.setEmpty();
151 rects.second.setEmpty();
152 }
153 return rects;
154 }
155
Alan Viverette7ad7d9092022-01-13 15:21:21 +0000156 private PaintCompat() {}
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -0800157}