Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package androidx.core.graphics; |
| 18 | |
Nader Jawad | 39bec90 | 2019-06-24 16:10:48 -0700 | [diff] [blame] | 19 | import static androidx.core.graphics.BlendModeUtils.obtainBlendModeFromCompat; |
| 20 | import static androidx.core.graphics.BlendModeUtils.obtainPorterDuffFromCompat; |
| 21 | |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 22 | import android.graphics.Paint; |
Nader Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 23 | import android.graphics.PorterDuff; |
Nader Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 24 | import android.graphics.PorterDuffXfermode; |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 25 | import android.graphics.Rect; |
| 26 | import android.os.Build; |
Aurimas Liutikas | 9dede51 | 2018-03-13 14:08:08 -0700 | [diff] [blame] | 27 | |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 28 | import androidx.annotation.NonNull; |
Nader Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 29 | import androidx.annotation.Nullable; |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 30 | import androidx.core.util.Pair; |
| 31 | |
| 32 | /** |
| 33 | * Helper for accessing features in {@link Paint}. |
| 34 | */ |
| 35 | public 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 Viverette | 7ad7d909 | 2022-01-13 15:21:21 +0000 | [diff] [blame] | 52 | return paint.hasGlyph(string); |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 53 | } |
| 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 Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 113 | /** |
| 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 Jawad | db1dcb1 | 2019-04-24 15:03:42 -0700 | [diff] [blame] | 121 | * @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 Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 124 | */ |
Nader Jawad | db1dcb1 | 2019-04-24 15:03:42 -0700 | [diff] [blame] | 125 | public static boolean setBlendMode(@NonNull Paint paint, @Nullable BlendModeCompat blendMode) { |
Clara Bayarri | d20f2c1 | 2019-04-29 15:04:04 +0100 | [diff] [blame] | 126 | if (Build.VERSION.SDK_INT >= 29) { |
Alan Viverette | 7ad7d909 | 2022-01-13 15:21:21 +0000 | [diff] [blame] | 127 | paint.setBlendMode(blendMode != null ? obtainBlendModeFromCompat(blendMode) : null); |
Nader Jawad | db1dcb1 | 2019-04-24 15:03:42 -0700 | [diff] [blame] | 128 | // All blend modes supported in Q |
| 129 | return true; |
| 130 | } else if (blendMode != null) { |
Nader Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 131 | PorterDuff.Mode mode = obtainPorterDuffFromCompat(blendMode); |
Nader Jawad | db1dcb1 | 2019-04-24 15:03:42 -0700 | [diff] [blame] | 132 | 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 Jawad | ee1f6f1 | 2019-03-12 15:11:17 -0700 | [diff] [blame] | 141 | } |
| 142 | } |
| 143 | |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 144 | 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 Viverette | 7ad7d909 | 2022-01-13 15:21:21 +0000 | [diff] [blame] | 156 | private PaintCompat() {} |
Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 157 | } |