MinTouchTargetHelper.java

/*
 * Copyright 2018 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.car.widget;

import android.graphics.Rect;
import android.view.TouchDelegate;
import android.view.View;

import androidx.annotation.NonNull;

/**
 * Helper class that will ensure that a given view meets a minimum touch target size that is
 * specified.
 */
class MinTouchTargetHelper {
    private MinTouchTargetHelper() {}

    /**
     * Sets up the view that will be checked and ensured to meet a minimum touch target size.
     *
     * @param view The view that to be checked; cannot be {@code null}.
     * @return A {@link TouchTargetSubject} that can be customized with touch target information.
     */
    static TouchTargetSubject ensureThat(View view) {
        if (view == null) {
            throw new IllegalArgumentException("View cannot be null.");
        }
        return new TouchTargetSubject(view);
    }

    /**
     * A class to encapsulate information concerning a view whose target target needs to be
     * expanded to meet a minimum touch size.
     *
     * <p>This class should not be created directly. Instead use
     * {@link MinTouchTargetHelper#ensureThat(View)} to get an instance of this class.
     */
    static class TouchTargetSubject {
        private View mSubjectView;

        TouchTargetSubject(@NonNull View subject) {
            mSubjectView = subject;
        }

        /**
         * Sets the minimum touch target size in pixels for the subject view. Calling this method
         * will also set up the framework to do this assurance.
         *
         * <p>Under the hood, this method uses a {@link TouchDelegate} to handle the delegation,
         * meaning that only one child view can be set per parent.
         *
         * @param size The minimum touch target size in pixels.
         */
        void hasMinTouchSize(int size) {
            if (size <= 0) {
                throw new IllegalStateException(
                        "Minimum touch target size must be greater than 0.");
            }

            if (!(mSubjectView.getParent() instanceof View)) {
                throw new IllegalStateException("Subject view does not have a valid parent of type"
                        + " View. Parent is: " + mSubjectView.getParent());
            }

            View parentView = (View) mSubjectView.getParent();

            // The TouchDelegate needs to be set after the subject view has been laid out in
            // order to get the hit Rect. Use a post() to ensure this.
            parentView.post(() -> {
                Rect rect = new Rect();
                mSubjectView.getHitRect(rect);

                // Ensure that the touch target for the icon is the minimum touch size.
                int hitWidth = Math.abs(rect.right - rect.left);
                if (hitWidth < size) {
                    int amountToIncrease = (size - hitWidth) / 2;
                    rect.left -= amountToIncrease;
                    rect.right += amountToIncrease;
                }

                int hitHeight = Math.abs(rect.top - rect.bottom);
                if (hitHeight < size) {
                    int amountToIncrease = (size - hitHeight) / 2;
                    rect.top -= amountToIncrease;
                    rect.bottom += amountToIncrease;
                }

                parentView.setTouchDelegate(new TouchDelegate(rect, mSubjectView));
            });
        }
    }
}