EmojiTextViewHelper.java

/*
 * 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 android.os.Build;
import android.text.InputFilter;
import android.text.method.PasswordTransformationMethod;
import android.text.method.TransformationMethod;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
import androidx.emoji.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) {
        Preconditions.checkNotNull(textView, "textView cannot be null");
        mHelper = Build.VERSION.SDK_INT >= 19 ? new HelperInternal19(textView)
                : new HelperInternal();
    }

    /**
     * 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.
     */
    @NonNull
    public InputFilter[] getFilters(@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);
    }

    /**
     * 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);
    }

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

        void updateTransformationMethod() {
            // do nothing
        }

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

        TransformationMethod wrapTransformationMethod(TransformationMethod transformationMethod) {
            return transformationMethod;
        }

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

    @RequiresApi(19)
    private static class HelperInternal19 extends HelperInternal {
        private final TextView mTextView;
        private final EmojiInputFilter mEmojiInputFilter;

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

        @Override
        void updateTransformationMethod() {
            final TransformationMethod tm = mTextView.getTransformationMethod();
            if (tm != null && !(tm instanceof PasswordTransformationMethod)) {
                mTextView.setTransformationMethod(wrapTransformationMethod(tm));
            }
        }

        @Override
        InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
            final int count = filters.length;
            for (int i = 0; i < count; i++) {
                if (filters[i] instanceof EmojiInputFilter) {
                    return filters;
                }
            }
            final InputFilter[] newFilters = new InputFilter[filters.length + 1];
            System.arraycopy(filters, 0, newFilters, 0, count);
            newFilters[count] = mEmojiInputFilter;
            return newFilters;
        }

        @Override
        TransformationMethod wrapTransformationMethod(TransformationMethod transformationMethod) {
            if (transformationMethod instanceof EmojiTransformationMethod) {
                return transformationMethod;
            }
            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();
            }
        }

    }
}