blob: c60a90cbf221a9bf852063a0e0a8f9b56759f5b5 [file] [log] [blame]
/*
* 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 android.support.text.emoji.widget;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.text.emoji.EmojiSpan;
import android.support.v4.util.Preconditions;
import android.text.Editable;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
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.
* <p>
* 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<WatcherWrapper> 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> T[] getSpans(int queryStart, int queryEnd, Class<T> 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;
}
}
}