public final class

EmojiTextViewHelper

extends java.lang.Object

 java.lang.Object

↳androidx.emoji2.viewsintegration.EmojiTextViewHelper

Gradle dependencies

compile group: 'androidx.emoji2', name: 'emoji2-views-helper', version: '1.5.0'

  • groupId: androidx.emoji2
  • artifactId: emoji2-views-helper
  • version: 1.5.0

Artifact androidx.emoji2:emoji2-views-helper:1.5.0 it located at Google repository (https://maven.google.com/)

Overview

Utility class to enhance custom TextView widgets with EmojiCompat.

 public class MyEmojiTextView extends TextView {
     public MyEmojiTextView(Context context) {
         super(context);
         init();
     }
     // ..
     private void init() {
         getEmojiTextViewHelper().updateTransformationMethod();
     }

      @Override
     public void setFilters(InputFilter[] filters) {
         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
     }

      @Override
     public void setAllCaps(boolean allCaps) {
         super.setAllCaps(allCaps);
         getEmojiTextViewHelper().setAllCaps(allCaps);
     }

     private EmojiTextViewHelper getEmojiTextViewHelper() {
         if (mEmojiTextViewHelper == null) {
             mEmojiTextViewHelper = new EmojiTextViewHelper(this);
         }
         return mEmojiTextViewHelper;
     }
 }
 

Summary

Constructors
publicEmojiTextViewHelper(TextView textView)

Default constructor.

publicEmojiTextViewHelper(TextView textView, boolean expectInitializedEmojiCompat)

Allows skipping of all processing until EmojiCompat.init is called.

Methods
public InputFiltergetFilters(InputFilter filters[])

Appends EmojiCompat InputFilters to the widget InputFilters.

public booleanisEnabled()

public voidsetAllCaps(boolean allCaps)

Call when allCaps is set on TextView.

public voidsetEnabled(boolean enabled)

When enabled, methods will have their documented behavior.

public voidupdateTransformationMethod()

Updates widget's TransformationMethod so that the transformed text can be processed.

public TransformationMethodwrapTransformationMethod(TransformationMethod transformationMethod)

Returns transformation method that can update the transformed text to display emojis.

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

Constructors

public EmojiTextViewHelper(TextView textView)

Default constructor.

Parameters:

textView: TextView instance

public EmojiTextViewHelper(TextView textView, boolean expectInitializedEmojiCompat)

Allows skipping of all processing until EmojiCompat.init is called. This is useful when integrating EmojiTextViewHelper into libraries that subclass TextView that do not have control over EmojiCompat initialization by the app that uses the TextView subclass. If this helper is initialized prior to EmojiCompat.init, the TextView it's configuring will not display emoji using EmojiCompat after init is called until the transformation method and filter are updated. The easiest way to do that is call EmojiTextViewHelper.setEnabled(boolean).

Parameters:

textView: TextView instance
expectInitializedEmojiCompat: if true, this helper will assume init has been called and throw if it has not. If false, the methods on this helper will have no effect until EmojiCompat.init is called.

Methods

public void updateTransformationMethod()

Updates widget's TransformationMethod so that the transformed text can be processed. Should be called in the widget constructor. When used on devices running API 18 or below, this method does nothing.

See also: EmojiTextViewHelper.wrapTransformationMethod(TransformationMethod)

public InputFilter getFilters(InputFilter filters[])

Appends EmojiCompat InputFilters to the widget InputFilters. Should be called by TextView to update the InputFilters. When used on devices running API 18 or below, this method returns filters that is given as a parameter.

Parameters:

filters: InputFilter array passed to TextView

Returns:

same copy if the array already contains EmojiCompat InputFilter. A new array copy if not.

public TransformationMethod wrapTransformationMethod(TransformationMethod transformationMethod)

Returns transformation method that can update the transformed text to display emojis. When used on devices running API 18 or below, this method returns transformationMethod that is given as a parameter.

Parameters:

transformationMethod: instance to be wrapped

public void setEnabled(boolean enabled)

When enabled, methods will have their documented behavior. When disabled, all methods will have no effect and emoji will not be processed. Setting this to disable will also have the side effect of setting both the transformation method and filter if enabled has changed since the last call. By default EmojiTextViewHelper is enabled. You do not need to call EmojiTextViewHelper.updateTransformationMethod() again after calling setEnabled.

Parameters:

enabled: if this helper should process emoji.

public void setAllCaps(boolean allCaps)

Call when allCaps is set on TextView. When used on devices running API 18 or below, this method does nothing.

Parameters:

allCaps: allCaps parameter passed to TextView

public boolean isEnabled()

Returns:

current enabled state for this helper

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.viewsintegration;

import android.text.InputFilter;
import android.text.method.PasswordTransformationMethod;
import android.text.method.TransformationMethod;
import android.util.SparseArray;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import androidx.emoji2.text.EmojiCompat;

/**
 * Utility class to enhance custom TextView widgets with {@link EmojiCompat}.
 * <pre>
 * public class MyEmojiTextView extends TextView {
 *     public MyEmojiTextView(Context context) {
 *         super(context);
 *         init();
 *     }
 *     // ..
 *     private void init() {
 *         getEmojiTextViewHelper().updateTransformationMethod();
 *     }
 *
 *     {@literal @}Override
 *     public void setFilters(InputFilter[] filters) {
 *         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
 *     }
 *
 *     {@literal @}Override
 *     public void setAllCaps(boolean allCaps) {
 *         super.setAllCaps(allCaps);
 *         getEmojiTextViewHelper().setAllCaps(allCaps);
 *     }
 *
 *     private EmojiTextViewHelper getEmojiTextViewHelper() {
 *         if (mEmojiTextViewHelper == null) {
 *             mEmojiTextViewHelper = new EmojiTextViewHelper(this);
 *         }
 *         return mEmojiTextViewHelper;
 *     }
 * }
 * </pre>
 */
public final class EmojiTextViewHelper {

    private final HelperInternal mHelper;

    /**
     * Default constructor.
     *
     * @param textView TextView instance
     */
    public EmojiTextViewHelper(@NonNull TextView textView) {
        this(textView, true);
    }

    /**
     * Allows skipping of all processing until EmojiCompat.init is called.
     *
     * This is useful when integrating EmojiTextViewHelper into libraries that subclass TextView
     * that do not have control over EmojiCompat initialization by the app that uses the TextView
     * subclass.
     *
     * If this helper is initialized prior to EmojiCompat.init, the TextView it's configuring
     * will not display emoji using EmojiCompat after init is called until the transformation
     * method and filter are updated. The easiest way to do that is call
     * {@link EmojiTextViewHelper#setEnabled(boolean)}.
     *
     * @param textView TextView instance
     * @param expectInitializedEmojiCompat if true, this helper will assume init has been called
     *                                     and throw if it has not. If false, the methods on this
     *                                     helper will have no effect until EmojiCompat.init is
     *                                     called.
     */
    public EmojiTextViewHelper(@NonNull TextView textView, boolean expectInitializedEmojiCompat) {
        Preconditions.checkNotNull(textView, "textView cannot be null");
        if (!expectInitializedEmojiCompat) {
            mHelper = new SkippingHelper19(textView);
        } else {
            mHelper = new HelperInternal19(textView);
        }
    }

    /**
     * Updates widget's TransformationMethod so that the transformed text can be processed.
     * Should be called in the widget constructor. When used on devices running API 18 or below,
     * this method does nothing.
     *
     * @see #wrapTransformationMethod(TransformationMethod)
     */
    public void updateTransformationMethod() {
        mHelper.updateTransformationMethod();
    }

    /**
     * Appends EmojiCompat InputFilters to the widget InputFilters. Should be called by {@link
     * TextView#setFilters(InputFilter[])} to update the InputFilters. When used on devices running
     * API 18 or below, this method returns {@code filters} that is given as a parameter.
     *
     * @param filters InputFilter array passed to {@link TextView#setFilters(InputFilter[])}
     *
     * @return same copy if the array already contains EmojiCompat InputFilter. A new array copy if
     * not.
     */
    @SuppressWarnings("ArrayReturn")
    @NonNull
    public InputFilter[] getFilters(
            @SuppressWarnings("ArrayReturn") @NonNull final InputFilter[] filters) {
        return mHelper.getFilters(filters);
    }

    /**
     * Returns transformation method that can update the transformed text to display emojis. When
     * used on devices running API 18 or below, this method returns {@code transformationMethod}
     * that is given as a parameter.
     *
     * @param transformationMethod instance to be wrapped
     */
    @Nullable
    public TransformationMethod wrapTransformationMethod(
            @Nullable TransformationMethod transformationMethod) {
        return mHelper.wrapTransformationMethod(transformationMethod);
    }

    /**
     * When enabled, methods will have their documented behavior.
     *
     * When disabled, all methods will have no effect and emoji will not be processed.
     *
     * Setting this to disable will also have the side effect of setting both the transformation
     * method and filter if enabled has changed since the last call. By default
     * EmojiTextViewHelper is enabled.
     *
     * You do not need to call {@link EmojiTextViewHelper#updateTransformationMethod()} again after
     * calling setEnabled.
     *
     * @param enabled if this helper should process emoji.
     */
    public void setEnabled(boolean enabled) {
        mHelper.setEnabled(enabled);
    }

    /**
     * Call when allCaps is set on TextView. When used on devices running API 18 or below, this
     * method does nothing.
     *
     * @param allCaps allCaps parameter passed to {@link TextView#setAllCaps(boolean)}
     */
    public void setAllCaps(boolean allCaps) {
        mHelper.setAllCaps(allCaps);
    }

    /**
     * @return current enabled state for this helper
     */
    public boolean isEnabled() {
        return mHelper.isEnabled();
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static class HelperInternal {

        void updateTransformationMethod() {
            // do nothing
        }

        @NonNull
        InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
            return filters;
        }

        @Nullable
        TransformationMethod wrapTransformationMethod(
                @Nullable TransformationMethod transformationMethod) {
            return transformationMethod;
        }

        void setAllCaps(boolean allCaps) {
            // do nothing
        }

        void setEnabled(boolean processEmoji) {
            // do nothing
        }

        public boolean isEnabled() {
            return false;
        }
    }

    /**
     * This helper allows EmojiTextViewHelper to skip all calls to EmojiCompat until
     * {@link EmojiCompat#isConfigured()} returns true on devices that are 19+.
     *
     * When isConfigured returns true, this delegates to {@link HelperInternal19} to provide
     * EmojiCompat behavior. This has the effect of making EmojiCompat calls a "no-op" when
     * EmojiCompat is not configured on a device.
     *
     * There is no mechanism to be informed when isConfigured becomes true as it will lead to
     * likely memory leaks in situations where isConfigured never becomes true, and it is the
     * responsibility of the caller to call
     * {@link EmojiTextViewHelper#updateTransformationMethod()} after configuring EmojiCompat if
     * TextView's using EmojiTextViewHelper are already displayed to the user.
     */
    private static class SkippingHelper19 extends HelperInternal {
        private final HelperInternal19 mHelperDelegate;

        SkippingHelper19(TextView textView) {
            mHelperDelegate = new HelperInternal19(textView);
        }

        private boolean skipBecauseEmojiCompatNotInitialized() {
            return !EmojiCompat.isConfigured();
        }

        /**
         * {@inheritDoc}
         *
         * This method will have no effect if !{@link EmojiCompat#isConfigured()}
         */
        @Override
        void updateTransformationMethod() {
            if (skipBecauseEmojiCompatNotInitialized()) {
                return;
            }
            mHelperDelegate.updateTransformationMethod();
        }

        /**
         * {@inheritDoc}
         *
         * This method will have no effect if !{@link EmojiCompat#isConfigured()}
         */
        @NonNull
        @Override
        InputFilter[] getFilters(@NonNull InputFilter[] filters) {
            if (skipBecauseEmojiCompatNotInitialized()) {
                return filters;
            }
            return mHelperDelegate.getFilters(filters);
        }

        /**
         * {@inheritDoc}
         *
         * This method will have no effect if !{@link EmojiCompat#isConfigured()}
         */
        @Nullable
        @Override
        TransformationMethod wrapTransformationMethod(
                @Nullable TransformationMethod transformationMethod) {
            if (skipBecauseEmojiCompatNotInitialized()) {
                return transformationMethod;
            }
            return mHelperDelegate.wrapTransformationMethod(transformationMethod);
        }

        /**
         * {@inheritDoc}
         *
         * This method will have no effect if !{@link EmojiCompat#isConfigured()}
         */
        @Override
        void setAllCaps(boolean allCaps) {
            if (skipBecauseEmojiCompatNotInitialized()) {
                return;
            }
            mHelperDelegate.setAllCaps(allCaps);
        }

        /**
         * {@inheritDoc}
         *
         * This method will track enabled, but have no other effect if
         * !{@link EmojiCompat#isConfigured()}
         */
        @Override
        void setEnabled(boolean processEmoji) {
            if (skipBecauseEmojiCompatNotInitialized()) {
                mHelperDelegate.setEnabledUnsafe(processEmoji);
            } else {
                mHelperDelegate.setEnabled(processEmoji);
            }
        }

        @Override
        public boolean isEnabled() {
            return mHelperDelegate.isEnabled();
        }
    }

    private static class HelperInternal19 extends HelperInternal {
        private final TextView mTextView;
        private final EmojiInputFilter mEmojiInputFilter;
        private boolean mEnabled;

        HelperInternal19(TextView textView) {
            mTextView = textView;
            mEnabled = true;
            mEmojiInputFilter = new EmojiInputFilter(textView);
        }


        @Override
        void updateTransformationMethod() {
            // since this is not a pure function, we need to have a side effect for both enabled
            // and disabled
            final TransformationMethod tm =
                    wrapTransformationMethod(mTextView.getTransformationMethod());
            mTextView.setTransformationMethod(tm);
        }

        /**
         * Call whenever mEnabled changes
         */
        private void updateFilters() {
            InputFilter[] oldFilters = mTextView.getFilters();
            mTextView.setFilters(getFilters(oldFilters));
        }

        @NonNull
        @Override
        InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
            if (!mEnabled) {
                // remove any EmojiInputFilter when disabled
                return removeEmojiInputFilterIfPresent(filters);
            } else {
                return addEmojiInputFilterIfMissing(filters);
            }
        }

        /**
         * Make sure that EmojiInputFilter is present in filters, or add it.
         *
         * @param filters to check
         * @return filters with mEmojiInputFilter added, if not previously present
         */
        @NonNull
        private InputFilter[] addEmojiInputFilterIfMissing(@NonNull InputFilter[] filters) {
            final int count = filters.length;
            for (int i = 0; i < count; i++) {
                if (filters[i] == mEmojiInputFilter) {
                    return filters;
                }
            }
            final InputFilter[] newFilters = new InputFilter[filters.length + 1];
            System.arraycopy(filters, 0, newFilters, 0, count);
            newFilters[count] = mEmojiInputFilter;
            return newFilters;
        }

        /**
         * Remove all EmojiInputFilter from filters
         *
         * @return filters.filter { it !== mEmojiInputFilter }
         */
        @NonNull
        private InputFilter[] removeEmojiInputFilterIfPresent(@NonNull InputFilter[] filters) {
            // find out the new size after removing (all) EmojiInputFilter
            SparseArray<InputFilter> filterSet = getEmojiInputFilterPositionArray(filters);
            if (filterSet.size() == 0) {
                return filters;
            }


            final int inCount = filters.length;
            int outCount = filters.length - filterSet.size();
            InputFilter[] result = new InputFilter[outCount];
            int destPosition = 0;
            for (int srcPosition = 0; srcPosition < inCount; srcPosition++) {
                if (filterSet.indexOfKey(srcPosition) < 0) {
                    result[destPosition] = filters[srcPosition];
                    destPosition++;
                }
            }
            return result;
        }

        /**
         * Populate a sparse array with true for all indexes that contain an EmojiInputFilter.
         */
        private SparseArray<InputFilter> getEmojiInputFilterPositionArray(
                @NonNull InputFilter[] filters) {
            SparseArray<InputFilter> result = new SparseArray<>(1);
            for (int pos = 0; pos < filters.length; pos++) {
                if (filters[pos] instanceof EmojiInputFilter) {
                    result.put(pos, filters[pos]);
                }
            }
            return result;
        }

        @Nullable
        @Override
        TransformationMethod wrapTransformationMethod(
                @Nullable TransformationMethod transformationMethod) {
            if (mEnabled) {
                return wrapForEnabled(transformationMethod);
            } else {
                return unwrapForDisabled(transformationMethod);
            }
        }

        /**
         * Unwrap EmojiTransformationMethods safely.
         */
        @Nullable
        private TransformationMethod unwrapForDisabled(
                @Nullable TransformationMethod transformationMethod) {
            if (transformationMethod instanceof EmojiTransformationMethod) {
                EmojiTransformationMethod etm =
                        (EmojiTransformationMethod) transformationMethod;
                return etm.getOriginalTransformationMethod();
            } else {
                return transformationMethod;
            }
        }

        /**
         * Wrap in EmojiTransformationMethod, but don't double wrap.
         *
         * This will not wrap {@link PasswordTransformationMethod}.
         */
        @NonNull
        private TransformationMethod wrapForEnabled(
                @Nullable TransformationMethod transformationMethod) {
            if (transformationMethod instanceof EmojiTransformationMethod) {
                return transformationMethod;
            } else if (transformationMethod instanceof PasswordTransformationMethod) {
                return transformationMethod;
            } else {
                return new EmojiTransformationMethod(transformationMethod);
            }
        }

        @Override
        void setAllCaps(boolean allCaps) {
            // When allCaps is set to false TextView sets the transformation method to be null. We
            // are only interested when allCaps is set to true in order to wrap the original method.
            if (allCaps) {
                updateTransformationMethod();
            }
        }

        @Override
        void setEnabled(boolean enabled) {
            mEnabled = enabled;
            updateTransformationMethod();
            updateFilters();
        }

        @Override
        public boolean isEnabled() {
            return mEnabled;
        }

        /**
         * Call to set enabled without side effects. Should only be used when EmojiCompat is not
         * initialized.
         *
         * @param processEmoji when true, this helper will process emoji
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        void setEnabledUnsafe(boolean processEmoji) {
            mEnabled = processEmoji;
        }
    }
}