1 /* 2 * Copyright 2018 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 package androidx.emoji.text; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20 import android.graphics.Canvas; 21 import android.graphics.Paint; 22 import android.graphics.Typeface; 23 24 import androidx.annotation.AnyThread; 25 import androidx.annotation.IntDef; 26 import androidx.annotation.IntRange; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.RequiresApi; 29 import androidx.annotation.RestrictTo; 30 import androidx.text.emoji.flatbuffer.MetadataItem; 31 import androidx.text.emoji.flatbuffer.MetadataList; 32 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 36 /** 37 * Information about a single emoji. 38 * 39 * @hide 40 */ 41 @RestrictTo(LIBRARY_GROUP) 42 @AnyThread 43 @RequiresApi(19) 44 public class EmojiMetadata { 45 /** 46 * Defines whether the system can render the emoji. 47 */ 48 @IntDef({HAS_GLYPH_UNKNOWN, HAS_GLYPH_ABSENT, HAS_GLYPH_EXISTS}) 49 @Retention(RetentionPolicy.SOURCE) 50 public @interface HasGlyph { 51 } 52 53 /** 54 * Not calculated on device yet. 55 */ 56 public static final int HAS_GLYPH_UNKNOWN = 0; 57 58 /** 59 * Device cannot render the emoji. 60 */ 61 public static final int HAS_GLYPH_ABSENT = 1; 62 63 /** 64 * Device can render the emoji. 65 */ 66 public static final int HAS_GLYPH_EXISTS = 2; 67 68 /** 69 * @see #getMetadataItem() 70 */ 71 private static final ThreadLocal<MetadataItem> sMetadataItem = new ThreadLocal<>(); 72 73 /** 74 * Index of the EmojiMetadata in {@link MetadataList}. 75 */ 76 private final int mIndex; 77 78 /** 79 * MetadataRepo that holds this instance. 80 */ 81 private final MetadataRepo mMetadataRepo; 82 83 /** 84 * Whether the system can render the emoji. Calculated at runtime on the device. 85 */ 86 @HasGlyph 87 private volatile int mHasGlyph = HAS_GLYPH_UNKNOWN; 88 89 EmojiMetadata(@NonNull final MetadataRepo metadataRepo, @IntRange(from = 0) final int index) { 90 mMetadataRepo = metadataRepo; 91 mIndex = index; 92 } 93 94 /** 95 * Draws the emoji represented by this EmojiMetadata onto a canvas with origin at (x,y), using 96 * the specified paint. 97 * 98 * @param canvas Canvas to be drawn 99 * @param x x-coordinate of the origin of the emoji being drawn 100 * @param y y-coordinate of the baseline of the emoji being drawn 101 * @param paint Paint used for the text (e.g. color, size, style) 102 */ 103 public void draw(@NonNull final Canvas canvas, final float x, final float y, 104 @NonNull final Paint paint) { 105 final Typeface typeface = mMetadataRepo.getTypeface(); 106 final Typeface oldTypeface = paint.getTypeface(); 107 paint.setTypeface(typeface); 108 // MetadataRepo.getEmojiCharArray() is a continous array of chars that is used to store the 109 // chars for emojis. since all emojis are mapped to a single codepoint, and since it is 2 110 // chars wide, we assume that the start index of the current emoji is mIndex * 2, and it is 111 // 2 chars long. 112 final int charArrayStartIndex = mIndex * 2; 113 canvas.drawText(mMetadataRepo.getEmojiCharArray(), charArrayStartIndex, 2, x, y, paint); 114 paint.setTypeface(oldTypeface); 115 } 116 117 /** 118 * @return return typeface to be used to render this metadata 119 */ 120 public Typeface getTypeface() { 121 return mMetadataRepo.getTypeface(); 122 } 123 124 /** 125 * @return a ThreadLocal instance of MetadataItem for this EmojiMetadata 126 */ 127 private MetadataItem getMetadataItem() { 128 MetadataItem result = sMetadataItem.get(); 129 if (result == null) { 130 result = new MetadataItem(); 131 sMetadataItem.set(result); 132 } 133 // MetadataList is a wrapper around the metadata ByteBuffer. MetadataItem is a wrapper with 134 // an index (pointer) on this ByteBuffer that represents a single emoji. Both are FlatBuffer 135 // classes that wraps a ByteBuffer and gives access to the information in it. In order not 136 // to create a wrapper class for each EmojiMetadata, we use mIndex as the index of the 137 // MetadataItem in the ByteBuffer. We need to reiniitalize the current thread local instance 138 // by executing the statement below. All the statement does is to set an int index in 139 // MetadataItem. the same instance is used by all EmojiMetadata classes in the same thread. 140 mMetadataRepo.getMetadataList().list(result, mIndex); 141 return result; 142 } 143 144 /** 145 * @return unique id for the emoji 146 */ 147 public int getId() { 148 return getMetadataItem().id(); 149 } 150 151 /** 152 * @return width of the emoji image 153 */ 154 public short getWidth() { 155 return getMetadataItem().width(); 156 } 157 158 /** 159 * @return height of the emoji image 160 */ 161 public short getHeight() { 162 return getMetadataItem().height(); 163 } 164 165 /** 166 * @return in which metadata version the emoji was added to metadata 167 */ 168 public short getCompatAdded() { 169 return getMetadataItem().compatAdded(); 170 } 171 172 /** 173 * @return first SDK that the support for this emoji was added 174 */ 175 public short getSdkAdded() { 176 return getMetadataItem().sdkAdded(); 177 } 178 179 /** 180 * @return whether the emoji is in Emoji Presentation by default (without emoji 181 * style selector 0xFE0F) 182 */ 183 @HasGlyph 184 public int getHasGlyph() { 185 return mHasGlyph; 186 } 187 188 /** 189 * Set whether the system can render the emoji. 190 * 191 * @param hasGlyph {@code true} if system can render the emoji 192 */ 193 public void setHasGlyph(boolean hasGlyph) { 194 mHasGlyph = hasGlyph ? HAS_GLYPH_EXISTS : HAS_GLYPH_ABSENT; 195 } 196 197 /** 198 * @return whether the emoji is in Emoji Presentation by default (without emoji 199 * style selector 0xFE0F) 200 */ 201 public boolean isDefaultEmoji() { 202 return getMetadataItem().emojiStyle(); 203 } 204 205 /** 206 * @param index index of the codepoint 207 * 208 * @return the codepoint at index 209 */ 210 public int getCodepointAt(int index) { 211 return getMetadataItem().codepoints(index); 212 } 213 214 /** 215 * @return the length of the codepoints for this emoji 216 */ 217 public int getCodepointsLength() { 218 return getMetadataItem().codepointsLength(); 219 } 220 221 @Override 222 public String toString() { 223 final StringBuilder builder = new StringBuilder(); 224 builder.append(super.toString()); 225 builder.append(", id:"); 226 builder.append(Integer.toHexString(getId())); 227 builder.append(", codepoints:"); 228 final int codepointsLength = getCodepointsLength(); 229 for (int i = 0; i < codepointsLength; i++) { 230 builder.append(Integer.toHexString(getCodepointAt(i))); 231 builder.append(" "); 232 } 233 return builder.toString(); 234 } 235 } 236