15504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska/*
2bd501400bacc36eb36e5b871334e859c51c2c2a6Aga Madurska * Copyright (C) 2017 The Android Open Source Project
35504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska *
45504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * Licensed under the Apache License, Version 2.0 (the "License");
55504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * you may not use this file except in compliance with the License.
65504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * You may obtain a copy of the License at
75504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska *
85504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska *      http://www.apache.org/licenses/LICENSE-2.0
95504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska *
105504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * Unless required by applicable law or agreed to in writing, software
115504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * distributed under the License is distributed on an "AS IS" BASIS,
125504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
135504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * See the License for the specific language governing permissions and
145504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska * limitations under the License.
155504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska */
1688e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska
17ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikaspackage androidx.wear.widget;
185504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
19bd501400bacc36eb36e5b871334e859c51c2c2a6Aga Madurskaimport android.content.Context;
205504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurskaimport android.graphics.Path;
215504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurskaimport android.graphics.PathMeasure;
22d75a466859fee504b717c529094e318d1278f831Aurimas Liutikasimport android.view.View;
23d75a466859fee504b717c529094e318d1278f831Aurimas Liutikas
24ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.annotation.VisibleForTesting;
25ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.recyclerview.widget.RecyclerView;
26ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport androidx.wear.R;
275504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
285504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska/**
2988e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska * An implementation of the {@link WearableLinearLayoutManager.LayoutCallback} aligning the children
3088e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska * of the associated {@link WearableRecyclerView} along a pre-defined vertical curve.
315504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska */
3288e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurskapublic class CurvingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback {
335504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private static final float EPSILON = 0.001f;
345504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
355504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private final Path mCurvePath;
365504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private final PathMeasure mPathMeasure;
375504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private int mCurvePathHeight;
385504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private int mXCurveOffset;
395504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private float mPathLength;
405504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private float mCurveBottom;
415504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private float mCurveTop;
425504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private float mLineGradient;
435504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private final float[] mPathPoints = new float[2];
445504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private final float[] mPathTangent = new float[2];
455504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private final float[] mAnchorOffsetXY = new float[2];
465504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
4788e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    private RecyclerView mParentView;
485504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private boolean mIsScreenRound;
495504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private int mLayoutWidth;
505504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private int mLayoutHeight;
515504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
5288e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    public CurvingLayoutCallback(Context context) {
535504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        mCurvePath = new Path();
545504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        mPathMeasure = new PathMeasure();
5588e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska        mIsScreenRound = context.getResources().getConfiguration().isScreenRound();
5688e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska        mXCurveOffset = context.getResources().getDimensionPixelSize(
5797f7aa4e4dd8f5c1f14d93d3f67540c72a5bbe57Aga Madurska                R.dimen.ws_wrv_curve_default_x_offset);
585504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    }
595504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
605504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    @Override
6188e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    public void onLayoutFinished(View child, RecyclerView parent) {
62bd501400bacc36eb36e5b871334e859c51c2c2a6Aga Madurska        if (mParentView != parent || (mParentView != null && (
63bd501400bacc36eb36e5b871334e859c51c2c2a6Aga Madurska                mParentView.getWidth() != parent.getWidth()
645504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                        || mParentView.getHeight() != parent.getHeight()))) {
655504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mParentView = parent;
665504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mLayoutWidth = mParentView.getWidth();
675504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mLayoutHeight = mParentView.getHeight();
685504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        }
695504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        if (mIsScreenRound) {
705504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            maybeSetUpCircularInitialLayout(mLayoutWidth, mLayoutHeight);
715504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mAnchorOffsetXY[0] = mXCurveOffset;
725504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mAnchorOffsetXY[1] = child.getHeight() / 2.0f;
735504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            adjustAnchorOffsetXY(child, mAnchorOffsetXY);
745504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float minCenter = -(float) child.getHeight() / 2;
755504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float maxCenter = mLayoutHeight + (float) child.getHeight() / 2;
765504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float range = maxCenter - minCenter;
775504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float verticalAnchor = (float) child.getTop() + mAnchorOffsetXY[1];
785504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float mYScrollProgress = (verticalAnchor + Math.abs(minCenter)) / range;
795504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
805504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mPathMeasure.getPosTan(mYScrollProgress * mPathLength, mPathPoints, mPathTangent);
815504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
825504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            boolean topClusterRisk =
8388e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska                    Math.abs(mPathPoints[1] - mCurveBottom) < EPSILON
8488e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska                            && minCenter < mPathPoints[1];
855504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            boolean bottomClusterRisk =
8688e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska                    Math.abs(mPathPoints[1] - mCurveTop) < EPSILON
8788e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska                            && maxCenter > mPathPoints[1];
8888e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska            // Continue offsetting the child along the straight-line part of the curve, if it
8988e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska            // has not gone off the screen when it reached the end of the original curve.
905504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            if (topClusterRisk || bottomClusterRisk) {
915504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                mPathPoints[1] = verticalAnchor;
925504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                mPathPoints[0] = (Math.abs(verticalAnchor) * mLineGradient);
935504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            }
945504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
955504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            // Offset the View to match the provided anchor point.
965504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            int newLeft = (int) (mPathPoints[0] - mAnchorOffsetXY[0]);
975504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            child.offsetLeftAndRight(newLeft - child.getLeft());
985504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            float verticalTranslation = mPathPoints[1] - verticalAnchor;
995504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            child.setTranslationY(verticalTranslation);
10088e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska        } else {
10188e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska            child.setTranslationY(0);
1025504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        }
1035504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    }
1045504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
1055504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    /**
10688e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     * Override this method if you wish to adjust the anchor coordinates for each child view
10788e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     * during a layout pass. In the override set the new desired anchor coordinates in
10888e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     * the provided array. The coordinates should be provided in relation to the child view.
1095504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska     *
1105504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska     * @param child          The child view to which the anchor coordinates will apply.
11188e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     * @param anchorOffsetXY The anchor coordinates for the provided child view, by default set
11288e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     *                       to a pre-defined constant on the horizontal axis and half of the
11388e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska     *                       child height on the vertical axis (vertical center).
1145504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska     */
1155504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) {
1165504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        return;
117acf36a11d0d08da4a94134e8a125e863f13cb625Aurimas Liutikas    }
11888e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska
11988e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    @VisibleForTesting
12088e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    void setRound(boolean isScreenRound) {
12188e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska        mIsScreenRound = isScreenRound;
12288e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    }
12388e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska
12488e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    @VisibleForTesting
12588e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska    void setOffset(int offset) {
12688e81a0c4edbbc6fec7a2d7362714c058f7ce619Aga Madurska        mXCurveOffset = offset;
1275504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    }
1285504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska
1295504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    /** Set up the initial layout for round screens. */
1305504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    private void maybeSetUpCircularInitialLayout(int width, int height) {
1315504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        // The values in this function are custom to the curve we use.
1325504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        if (mCurvePathHeight != height) {
1335504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePathHeight = height;
1345504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurveBottom = -0.048f * height;
1355504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurveTop = 1.048f * height;
1365504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mLineGradient = 0.5f / 0.048f;
1375504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.reset();
1385504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.moveTo(0.5f * width, mCurveBottom);
1395504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.lineTo(0.34f * width, 0.075f * height);
1405504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.cubicTo(
1415504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.22f * width, 0.17f * height, 0.13f * width, 0.32f * height, 0.13f * width,
1425504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    height / 2);
1435504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.cubicTo(
1445504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.13f * width,
1455504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.68f * height,
1465504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.22f * width,
1475504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.83f * height,
1485504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.34f * width,
1495504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska                    0.925f * height);
1505504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mCurvePath.lineTo(width / 2, mCurveTop);
1515504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mPathMeasure.setPath(mCurvePath, false);
1525504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska            mPathLength = mPathMeasure.getLength();
1535504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska        }
1545504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska    }
1555504220f38bb0552e1d8def09fb1b9a118264b45Aga Madurska}
156