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 */ 16package android.support.text.emoji.widget; 17 18import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20import android.support.annotation.NonNull; 21import android.support.annotation.Nullable; 22import android.support.annotation.RestrictTo; 23import android.support.text.emoji.EmojiSpan; 24import android.support.v4.util.Preconditions; 25import android.text.Editable; 26import android.text.SpanWatcher; 27import android.text.Spannable; 28import android.text.SpannableStringBuilder; 29import android.text.TextWatcher; 30 31import java.lang.reflect.Array; 32import java.util.ArrayList; 33import java.util.List; 34import java.util.concurrent.atomic.AtomicInteger; 35 36/** 37 * When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance 38 * of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject 39 * (WatcherWrapper) that implements the same interfaces. 40 * <p> 41 * During a span change event WatcherWrapper’s functions are fired, it checks if the span is an 42 * EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs 43 * ChangeWatcher only once at the end of the edit. Important point is, the block operation is 44 * applied only for EmojiSpans. Therefore any other span change operation works the same way as in 45 * the framework. 46 * 47 * @hide 48 * @see EmojiEditableFactory 49 */ 50@RestrictTo(LIBRARY_GROUP) 51public final class SpannableBuilder extends SpannableStringBuilder { 52 /** 53 * DynamicLayout$ChangeWatcher class. 54 */ 55 private final Class<?> mWatcherClass; 56 57 /** 58 * All WatcherWrappers. 59 */ 60 private final List<WatcherWrapper> mWatchers = new ArrayList<>(); 61 62 /** 63 * @hide 64 */ 65 @RestrictTo(LIBRARY_GROUP) 66 SpannableBuilder(@NonNull Class<?> watcherClass) { 67 Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); 68 mWatcherClass = watcherClass; 69 } 70 71 /** 72 * @hide 73 */ 74 @RestrictTo(LIBRARY_GROUP) 75 SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) { 76 super(text); 77 Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); 78 mWatcherClass = watcherClass; 79 } 80 81 /** 82 * @hide 83 */ 84 @RestrictTo(LIBRARY_GROUP) 85 SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text, int start, 86 int end) { 87 super(text, start, end); 88 Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); 89 mWatcherClass = watcherClass; 90 } 91 92 /** 93 * @hide 94 */ 95 @RestrictTo(LIBRARY_GROUP) 96 static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) { 97 return new SpannableBuilder(clazz, text); 98 } 99 100 /** 101 * Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher. 102 * 103 * @param object mObject to be checked 104 * 105 * @return true if mObject is instance of the DynamicLayout$ChangeWatcher. 106 */ 107 private boolean isWatcher(@Nullable Object object) { 108 return object != null && isWatcher(object.getClass()); 109 } 110 111 /** 112 * Checks whether the class is DynamicLayout$ChangeWatcher. 113 * 114 * @param clazz class to be checked 115 * 116 * @return true if class is DynamicLayout$ChangeWatcher. 117 */ 118 private boolean isWatcher(@NonNull Class<?> clazz) { 119 return mWatcherClass == clazz; 120 } 121 122 @Override 123 public CharSequence subSequence(int start, int end) { 124 return new SpannableBuilder(mWatcherClass, this, start, end); 125 } 126 127 /** 128 * If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in 129 * another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set 130 * this new mObject as the span. 131 */ 132 @Override 133 public void setSpan(Object what, int start, int end, int flags) { 134 if (isWatcher(what)) { 135 final WatcherWrapper span = new WatcherWrapper(what); 136 mWatchers.add(span); 137 what = span; 138 } 139 super.setSpan(what, start, end, flags); 140 } 141 142 /** 143 * If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the 144 * correct Object that the client has set. 145 */ 146 @SuppressWarnings("unchecked") 147 @Override 148 public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { 149 if (isWatcher(kind)) { 150 final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd, 151 WatcherWrapper.class); 152 final T[] result = (T[]) Array.newInstance(kind, spans.length); 153 for (int i = 0; i < spans.length; i++) { 154 result[i] = (T) spans[i].mObject; 155 } 156 return result; 157 } 158 return super.getSpans(queryStart, queryEnd, kind); 159 } 160 161 /** 162 * If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper 163 * instead. 164 */ 165 @Override 166 public void removeSpan(Object what) { 167 final WatcherWrapper watcher; 168 if (isWatcher(what)) { 169 watcher = getWatcherFor(what); 170 if (watcher != null) { 171 what = watcher; 172 } 173 } else { 174 watcher = null; 175 } 176 177 super.removeSpan(what); 178 179 if (watcher != null) { 180 mWatchers.remove(watcher); 181 } 182 } 183 184 /** 185 * Return the correct start for the DynamicLayout$ChangeWatcher span. 186 */ 187 @Override 188 public int getSpanStart(Object tag) { 189 if (isWatcher(tag)) { 190 final WatcherWrapper watcher = getWatcherFor(tag); 191 if (watcher != null) { 192 tag = watcher; 193 } 194 } 195 return super.getSpanStart(tag); 196 } 197 198 /** 199 * Return the correct end for the DynamicLayout$ChangeWatcher span. 200 */ 201 @Override 202 public int getSpanEnd(Object tag) { 203 if (isWatcher(tag)) { 204 final WatcherWrapper watcher = getWatcherFor(tag); 205 if (watcher != null) { 206 tag = watcher; 207 } 208 } 209 return super.getSpanEnd(tag); 210 } 211 212 /** 213 * Return the correct flags for the DynamicLayout$ChangeWatcher span. 214 */ 215 @Override 216 public int getSpanFlags(Object tag) { 217 if (isWatcher(tag)) { 218 final WatcherWrapper watcher = getWatcherFor(tag); 219 if (watcher != null) { 220 tag = watcher; 221 } 222 } 223 return super.getSpanFlags(tag); 224 } 225 226 /** 227 * Return the correct transition for the DynamicLayout$ChangeWatcher span. 228 */ 229 @Override 230 public int nextSpanTransition(int start, int limit, Class type) { 231 if (isWatcher(type)) { 232 type = WatcherWrapper.class; 233 } 234 return super.nextSpanTransition(start, limit, type); 235 } 236 237 /** 238 * Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher. 239 * 240 * @param object DynamicLayout$ChangeWatcher mObject 241 * 242 * @return WatcherWrapper that wraps the mObject. 243 */ 244 private WatcherWrapper getWatcherFor(Object object) { 245 for (int i = 0; i < mWatchers.size(); i++) { 246 WatcherWrapper watcher = mWatchers.get(i); 247 if (watcher.mObject == object) { 248 return watcher; 249 } 250 } 251 return null; 252 } 253 254 /** 255 * @hide 256 */ 257 @RestrictTo(LIBRARY_GROUP) 258 public void beginBatchEdit() { 259 blockWatchers(); 260 } 261 262 /** 263 * @hide 264 */ 265 @RestrictTo(LIBRARY_GROUP) 266 public void endBatchEdit() { 267 unblockwatchers(); 268 fireWatchers(); 269 } 270 271 /** 272 * Block all watcher wrapper events. 273 */ 274 private void blockWatchers() { 275 for (int i = 0; i < mWatchers.size(); i++) { 276 mWatchers.get(i).blockCalls(); 277 } 278 } 279 280 /** 281 * Unblock all watcher wrapper events. 282 */ 283 private void unblockwatchers() { 284 for (int i = 0; i < mWatchers.size(); i++) { 285 mWatchers.get(i).unblockCalls(); 286 } 287 } 288 289 /** 290 * Unblock all watcher wrapper events. Called by editing operations, namely 291 * {@link SpannableStringBuilder#replace(int, int, CharSequence)}. 292 */ 293 private void fireWatchers() { 294 for (int i = 0; i < mWatchers.size(); i++) { 295 mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length()); 296 } 297 } 298 299 @Override 300 public SpannableStringBuilder replace(int start, int end, CharSequence tb) { 301 blockWatchers(); 302 super.replace(start, end, tb); 303 unblockwatchers(); 304 return this; 305 } 306 307 @Override 308 public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, 309 int tbend) { 310 blockWatchers(); 311 super.replace(start, end, tb, tbstart, tbend); 312 unblockwatchers(); 313 return this; 314 } 315 316 @Override 317 public SpannableStringBuilder insert(int where, CharSequence tb) { 318 super.insert(where, tb); 319 return this; 320 } 321 322 @Override 323 public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) { 324 super.insert(where, tb, start, end); 325 return this; 326 } 327 328 @Override 329 public SpannableStringBuilder delete(int start, int end) { 330 super.delete(start, end); 331 return this; 332 } 333 334 @Override 335 public SpannableStringBuilder append(CharSequence text) { 336 super.append(text); 337 return this; 338 } 339 340 @Override 341 public SpannableStringBuilder append(char text) { 342 super.append(text); 343 return this; 344 } 345 346 @Override 347 public SpannableStringBuilder append(CharSequence text, int start, int end) { 348 super.append(text, start, end); 349 return this; 350 } 351 352 @Override 353 public SpannableStringBuilder append(CharSequence text, Object what, int flags) { 354 super.append(text, what, flags); 355 return this; 356 } 357 358 /** 359 * Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout. 360 */ 361 private static class WatcherWrapper implements TextWatcher, SpanWatcher { 362 private final Object mObject; 363 private final AtomicInteger mBlockCalls = new AtomicInteger(0); 364 365 WatcherWrapper(Object object) { 366 this.mObject = object; 367 } 368 369 @Override 370 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 371 ((TextWatcher) mObject).beforeTextChanged(s, start, count, after); 372 } 373 374 @Override 375 public void onTextChanged(CharSequence s, int start, int before, int count) { 376 ((TextWatcher) mObject).onTextChanged(s, start, before, count); 377 } 378 379 @Override 380 public void afterTextChanged(Editable s) { 381 ((TextWatcher) mObject).afterTextChanged(s); 382 } 383 384 /** 385 * Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation 386 * (mBlockCalls is set) and the span that is added is an EmojiSpan. 387 */ 388 @Override 389 public void onSpanAdded(Spannable text, Object what, int start, int end) { 390 if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { 391 return; 392 } 393 ((SpanWatcher) mObject).onSpanAdded(text, what, start, end); 394 } 395 396 /** 397 * Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation 398 * (mBlockCalls is set) and the span that is added is an EmojiSpan. 399 */ 400 @Override 401 public void onSpanRemoved(Spannable text, Object what, int start, int end) { 402 if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { 403 return; 404 } 405 ((SpanWatcher) mObject).onSpanRemoved(text, what, start, end); 406 } 407 408 /** 409 * Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation 410 * (mBlockCalls is set) and the span that is added is an EmojiSpan. 411 */ 412 @Override 413 public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, 414 int nend) { 415 if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { 416 return; 417 } 418 ((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend); 419 } 420 421 final void blockCalls() { 422 mBlockCalls.incrementAndGet(); 423 } 424 425 final void unblockCalls() { 426 mBlockCalls.decrementAndGet(); 427 } 428 429 private boolean isEmojiSpan(final Object span) { 430 return span instanceof EmojiSpan; 431 } 432 } 433 434} 435