/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.emoji.widget; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.text.Editable; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextWatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.core.util.Preconditions; import androidx.emoji.text.EmojiSpan; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance * of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject * (WatcherWrapper) that implements the same interfaces. *

* During a span change event WatcherWrapper’s functions are fired, it checks if the span is an * EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs * ChangeWatcher only once at the end of the edit. Important point is, the block operation is * applied only for EmojiSpans. Therefore any other span change operation works the same way as in * the framework. * * @hide * @see EmojiEditableFactory */ @RestrictTo(LIBRARY_GROUP) public final class SpannableBuilder extends SpannableStringBuilder { /** * DynamicLayout$ChangeWatcher class. */ private final Class mWatcherClass; /** * All WatcherWrappers. */ private final List mWatchers = new ArrayList<>(); /** * @hide */ @RestrictTo(LIBRARY_GROUP) SpannableBuilder(@NonNull Class watcherClass) { Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); mWatcherClass = watcherClass; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) SpannableBuilder(@NonNull Class watcherClass, @NonNull CharSequence text) { super(text); Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); mWatcherClass = watcherClass; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) SpannableBuilder(@NonNull Class watcherClass, @NonNull CharSequence text, int start, int end) { super(text, start, end); Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null"); mWatcherClass = watcherClass; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) static SpannableBuilder create(@NonNull Class clazz, @NonNull CharSequence text) { return new SpannableBuilder(clazz, text); } /** * Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher. * * @param object mObject to be checked * * @return true if mObject is instance of the DynamicLayout$ChangeWatcher. */ private boolean isWatcher(@Nullable Object object) { return object != null && isWatcher(object.getClass()); } /** * Checks whether the class is DynamicLayout$ChangeWatcher. * * @param clazz class to be checked * * @return true if class is DynamicLayout$ChangeWatcher. */ private boolean isWatcher(@NonNull Class clazz) { return mWatcherClass == clazz; } @Override public CharSequence subSequence(int start, int end) { return new SpannableBuilder(mWatcherClass, this, start, end); } /** * If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in * another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set * this new mObject as the span. */ @Override public void setSpan(Object what, int start, int end, int flags) { if (isWatcher(what)) { final WatcherWrapper span = new WatcherWrapper(what); mWatchers.add(span); what = span; } super.setSpan(what, start, end, flags); } /** * If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the * correct Object that the client has set. */ @SuppressWarnings("unchecked") @Override public T[] getSpans(int queryStart, int queryEnd, Class kind) { if (isWatcher(kind)) { final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd, WatcherWrapper.class); final T[] result = (T[]) Array.newInstance(kind, spans.length); for (int i = 0; i < spans.length; i++) { result[i] = (T) spans[i].mObject; } return result; } return super.getSpans(queryStart, queryEnd, kind); } /** * If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper * instead. */ @Override public void removeSpan(Object what) { final WatcherWrapper watcher; if (isWatcher(what)) { watcher = getWatcherFor(what); if (watcher != null) { what = watcher; } } else { watcher = null; } super.removeSpan(what); if (watcher != null) { mWatchers.remove(watcher); } } /** * Return the correct start for the DynamicLayout$ChangeWatcher span. */ @Override public int getSpanStart(Object tag) { if (isWatcher(tag)) { final WatcherWrapper watcher = getWatcherFor(tag); if (watcher != null) { tag = watcher; } } return super.getSpanStart(tag); } /** * Return the correct end for the DynamicLayout$ChangeWatcher span. */ @Override public int getSpanEnd(Object tag) { if (isWatcher(tag)) { final WatcherWrapper watcher = getWatcherFor(tag); if (watcher != null) { tag = watcher; } } return super.getSpanEnd(tag); } /** * Return the correct flags for the DynamicLayout$ChangeWatcher span. */ @Override public int getSpanFlags(Object tag) { if (isWatcher(tag)) { final WatcherWrapper watcher = getWatcherFor(tag); if (watcher != null) { tag = watcher; } } return super.getSpanFlags(tag); } /** * Return the correct transition for the DynamicLayout$ChangeWatcher span. */ @Override public int nextSpanTransition(int start, int limit, Class type) { if (isWatcher(type)) { type = WatcherWrapper.class; } return super.nextSpanTransition(start, limit, type); } /** * Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher. * * @param object DynamicLayout$ChangeWatcher mObject * * @return WatcherWrapper that wraps the mObject. */ private WatcherWrapper getWatcherFor(Object object) { for (int i = 0; i < mWatchers.size(); i++) { WatcherWrapper watcher = mWatchers.get(i); if (watcher.mObject == object) { return watcher; } } return null; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) public void beginBatchEdit() { blockWatchers(); } /** * @hide */ @RestrictTo(LIBRARY_GROUP) public void endBatchEdit() { unblockwatchers(); fireWatchers(); } /** * Block all watcher wrapper events. */ private void blockWatchers() { for (int i = 0; i < mWatchers.size(); i++) { mWatchers.get(i).blockCalls(); } } /** * Unblock all watcher wrapper events. */ private void unblockwatchers() { for (int i = 0; i < mWatchers.size(); i++) { mWatchers.get(i).unblockCalls(); } } /** * Unblock all watcher wrapper events. Called by editing operations, namely * {@link SpannableStringBuilder#replace(int, int, CharSequence)}. */ private void fireWatchers() { for (int i = 0; i < mWatchers.size(); i++) { mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length()); } } @Override public SpannableStringBuilder replace(int start, int end, CharSequence tb) { blockWatchers(); super.replace(start, end, tb); unblockwatchers(); return this; } @Override public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) { blockWatchers(); super.replace(start, end, tb, tbstart, tbend); unblockwatchers(); return this; } @Override public SpannableStringBuilder insert(int where, CharSequence tb) { super.insert(where, tb); return this; } @Override public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) { super.insert(where, tb, start, end); return this; } @Override public SpannableStringBuilder delete(int start, int end) { super.delete(start, end); return this; } @Override public SpannableStringBuilder append(CharSequence text) { super.append(text); return this; } @Override public SpannableStringBuilder append(char text) { super.append(text); return this; } @Override public SpannableStringBuilder append(CharSequence text, int start, int end) { super.append(text, start, end); return this; } @Override public SpannableStringBuilder append(CharSequence text, Object what, int flags) { super.append(text, what, flags); return this; } /** * Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout. */ private static class WatcherWrapper implements TextWatcher, SpanWatcher { private final Object mObject; private final AtomicInteger mBlockCalls = new AtomicInteger(0); WatcherWrapper(Object object) { this.mObject = object; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { ((TextWatcher) mObject).beforeTextChanged(s, start, count, after); } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { ((TextWatcher) mObject).onTextChanged(s, start, before, count); } @Override public void afterTextChanged(Editable s) { ((TextWatcher) mObject).afterTextChanged(s); } /** * Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation * (mBlockCalls is set) and the span that is added is an EmojiSpan. */ @Override public void onSpanAdded(Spannable text, Object what, int start, int end) { if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { return; } ((SpanWatcher) mObject).onSpanAdded(text, what, start, end); } /** * Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation * (mBlockCalls is set) and the span that is added is an EmojiSpan. */ @Override public void onSpanRemoved(Spannable text, Object what, int start, int end) { if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { return; } ((SpanWatcher) mObject).onSpanRemoved(text, what, start, end); } /** * Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation * (mBlockCalls is set) and the span that is added is an EmojiSpan. */ @Override public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) { if (mBlockCalls.get() > 0 && isEmojiSpan(what)) { return; } ((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend); } final void blockCalls() { mBlockCalls.incrementAndGet(); } final void unblockCalls() { mBlockCalls.decrementAndGet(); } private boolean isEmojiSpan(final Object span) { return span instanceof EmojiSpan; } } }