public final class

SpannableBuilder

extends SpannableStringBuilder

 java.lang.Object

↳SpannableStringBuilder

↳androidx.emoji2.text.SpannableBuilder

Gradle dependencies

compile group: 'androidx.emoji2', name: 'emoji2', version: '1.2.0-alpha04'

  • groupId: androidx.emoji2
  • artifactId: emoji2
  • version: 1.2.0-alpha04

Artifact androidx.emoji2:emoji2:1.2.0-alpha04 it located at Google repository (https://maven.google.com/)

Overview

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.

Summary

Methods
public SpannableStringBuilderappend(char text)

public SpannableStringBuilderappend(java.lang.CharSequence text)

public SpannableStringBuilderappend(java.lang.CharSequence text, int start, int end)

public SpannableStringBuilderappend(java.lang.CharSequence text, java.lang.Object what, int flags)

public voidbeginBatchEdit()

public static SpannableBuildercreate(java.lang.Class<java.lang.Object> clazz, java.lang.CharSequence text)

public SpannableStringBuilderdelete(int start, int end)

public voidendBatchEdit()

public intgetSpanEnd(java.lang.Object tag)

Return the correct end for the DynamicLayout$ChangeWatcher span.

public intgetSpanFlags(java.lang.Object tag)

Return the correct flags for the DynamicLayout$ChangeWatcher span.

public java.lang.ObjectgetSpans(int queryStart, int queryEnd, java.lang.Class<java.lang.Object> kind)

If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the correct Object that the client has set.

public intgetSpanStart(java.lang.Object tag)

Return the correct start for the DynamicLayout$ChangeWatcher span.

public SpannableStringBuilderinsert(int where, java.lang.CharSequence tb)

public SpannableStringBuilderinsert(int where, java.lang.CharSequence tb, int start, int end)

public intnextSpanTransition(int start, int limit, java.lang.Class type)

Return the correct transition for the DynamicLayout$ChangeWatcher span.

public voidremoveSpan(java.lang.Object what)

If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper instead.

public SpannableStringBuilderreplace(int start, int end, java.lang.CharSequence tb)

public SpannableStringBuilderreplace(int start, int end, java.lang.CharSequence tb, int tbstart, int tbend)

public voidsetSpan(java.lang.Object what, int start, int end, int flags)

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.

public java.lang.CharSequencesubSequence(int start, int end)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static SpannableBuilder create(java.lang.Class<java.lang.Object> clazz, java.lang.CharSequence text)

public java.lang.CharSequence subSequence(int start, int end)

public void setSpan(java.lang.Object what, int start, int end, int flags)

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.

public java.lang.Object getSpans(int queryStart, int queryEnd, java.lang.Class<java.lang.Object> kind)

If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the correct Object that the client has set.

public void removeSpan(java.lang.Object what)

If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper instead.

public int getSpanStart(java.lang.Object tag)

Return the correct start for the DynamicLayout$ChangeWatcher span.

public int getSpanEnd(java.lang.Object tag)

Return the correct end for the DynamicLayout$ChangeWatcher span.

public int getSpanFlags(java.lang.Object tag)

Return the correct flags for the DynamicLayout$ChangeWatcher span.

public int nextSpanTransition(int start, int limit, java.lang.Class type)

Return the correct transition for the DynamicLayout$ChangeWatcher span.

public void beginBatchEdit()

public void endBatchEdit()

public SpannableStringBuilder replace(int start, int end, java.lang.CharSequence tb)

public SpannableStringBuilder replace(int start, int end, java.lang.CharSequence tb, int tbstart, int tbend)

public SpannableStringBuilder insert(int where, java.lang.CharSequence tb)

public SpannableStringBuilder insert(int where, java.lang.CharSequence tb, int start, int end)

public SpannableStringBuilder delete(int start, int end)

public SpannableStringBuilder append(java.lang.CharSequence text)

public SpannableStringBuilder append(char text)

public SpannableStringBuilder append(java.lang.CharSequence text, int start, int end)

public SpannableStringBuilder append(java.lang.CharSequence text, java.lang.Object what, int flags)

Source

/*
 * Copyright 2021 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.emoji2.text;

import android.annotation.SuppressLint;
import android.os.Build;
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 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
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SpannableBuilder extends SpannableStringBuilder {
    /**
     * DynamicLayout$ChangeWatcher class.
     */
    private final @NonNull Class<?> mWatcherClass;

    /**
     * All WatcherWrappers.
     */
    private final @NonNull List<WatcherWrapper> mWatchers = new ArrayList<>();

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    SpannableBuilder(@NonNull Class<?> watcherClass) {
        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
        mWatcherClass = watcherClass;
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) {
        super(text);
        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
        mWatcherClass = watcherClass;
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    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
     */
    @NonNull
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public 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;
    }

    @SuppressLint("UnknownNullness")
    @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(@Nullable 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.
     */
    @SuppressLint("UnknownNullness")
    @SuppressWarnings("unchecked")
    @Override
    public <T> T[] getSpans(int queryStart, int queryEnd, @NonNull 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(@Nullable 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(@Nullable 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(@Nullable 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(@Nullable 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, @Nullable Class type) {
        if (type == null || 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(RestrictTo.Scope.LIBRARY)
    public void beginBatchEdit() {
        blockWatchers();
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    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());
        }
    }

    @SuppressLint("UnknownNullness")
    @Override
    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
        blockWatchers();
        super.replace(start, end, tb);
        unblockwatchers();
        return this;
    }

    @SuppressLint("UnknownNullness")
    @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;
    }

    @SuppressLint("UnknownNullness")
    @Override
    public SpannableStringBuilder insert(int where, CharSequence tb) {
        super.insert(where, tb);
        return this;
    }

    @SuppressLint("UnknownNullness")
    @Override
    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
        super.insert(where, tb, start, end);
        return this;
    }

    @SuppressLint("UnknownNullness")
    @Override
    public SpannableStringBuilder delete(int start, int end) {
        super.delete(start, end);
        return this;
    }

    @NonNull
    @Override
    public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
        super.append(text);
        return this;
    }

    @NonNull
    @Override
    public SpannableStringBuilder append(char text) {
        super.append(text);
        return this;
    }

    @NonNull
    @Override
    public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
            int start,
            int end) {
        super.append(text, start, end);
        return this;
    }

    @SuppressLint("UnknownNullness")
    @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 {
        @SuppressWarnings("WeakerAccess") /* synthetic access */
        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;
            }
            // workaround for platform bug fixed in Android P
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
                // b/67926915 start cannot be determined, fallback to reflow from start instead
                // of causing an exception.

                // emoji2 bug b/216891011
                if (ostart > oend) {
                    ostart = 0;
                }
                if (nstart > nend) {
                    nstart = 0;
                }
            }
            ((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;
        }
    }

}