public final class

SpannableBuilder

extends SpannableStringBuilder

 java.lang.Object

↳SpannableStringBuilder

↳androidx.emoji.widget.SpannableBuilder

Gradle dependencies

compile group: 'androidx.emoji', name: 'emoji', version: '1.2.0-alpha03'

  • groupId: androidx.emoji
  • artifactId: emoji
  • version: 1.2.0-alpha03

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

Androidx artifact mapping:

androidx.emoji:emoji com.android.support:support-emoji

Androidx class mapping:

androidx.emoji.widget.SpannableBuilder android.support.text.emoji.widget.SpannableBuilder

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 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 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 (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_PREFIX;

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.
 * <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_PREFIX)
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_PREFIX)
    SpannableBuilder(@NonNull Class<?> watcherClass) {
        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
        mWatcherClass = watcherClass;
    }

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

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    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_PREFIX)
    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_PREFIX)
    public void beginBatchEdit() {
        blockWatchers();
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    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 {
        @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;
            }
            ((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;
        }
    }

}