1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.wear.widget;
18 
19 import android.content.Context;
20 import android.graphics.Path;
21 import android.graphics.PathMeasure;
22 import android.view.View;
23 
24 import androidx.annotation.VisibleForTesting;
25 import androidx.recyclerview.widget.RecyclerView;
26 import androidx.wear.R;
27 
28 /**
29  * An implementation of the {@link WearableLinearLayoutManager.LayoutCallback} aligning the children
30  * of the associated {@link WearableRecyclerView} along a pre-defined vertical curve.
31  */
32 public class CurvingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback {
33     private static final float EPSILON = 0.001f;
34 
35     private final Path mCurvePath;
36     private final PathMeasure mPathMeasure;
37     private int mCurvePathHeight;
38     private int mXCurveOffset;
39     private float mPathLength;
40     private float mCurveBottom;
41     private float mCurveTop;
42     private float mLineGradient;
43     private final float[] mPathPoints = new float[2];
44     private final float[] mPathTangent = new float[2];
45     private final float[] mAnchorOffsetXY = new float[2];
46 
47     private RecyclerView mParentView;
48     private boolean mIsScreenRound;
49     private int mLayoutWidth;
50     private int mLayoutHeight;
51 
CurvingLayoutCallback(Context context)52     public CurvingLayoutCallback(Context context) {
53         mCurvePath = new Path();
54         mPathMeasure = new PathMeasure();
55         mIsScreenRound = context.getResources().getConfiguration().isScreenRound();
56         mXCurveOffset = context.getResources().getDimensionPixelSize(
57                 R.dimen.ws_wrv_curve_default_x_offset);
58     }
59 
60     @Override
onLayoutFinished(View child, RecyclerView parent)61     public void onLayoutFinished(View child, RecyclerView parent) {
62         if (mParentView != parent || (mParentView != null && (
63                 mParentView.getWidth() != parent.getWidth()
64                         || mParentView.getHeight() != parent.getHeight()))) {
65             mParentView = parent;
66             mLayoutWidth = mParentView.getWidth();
67             mLayoutHeight = mParentView.getHeight();
68         }
69         if (mIsScreenRound) {
70             maybeSetUpCircularInitialLayout(mLayoutWidth, mLayoutHeight);
71             mAnchorOffsetXY[0] = mXCurveOffset;
72             mAnchorOffsetXY[1] = child.getHeight() / 2.0f;
73             adjustAnchorOffsetXY(child, mAnchorOffsetXY);
74             float minCenter = -(float) child.getHeight() / 2;
75             float maxCenter = mLayoutHeight + (float) child.getHeight() / 2;
76             float range = maxCenter - minCenter;
77             float verticalAnchor = (float) child.getTop() + mAnchorOffsetXY[1];
78             float mYScrollProgress = (verticalAnchor + Math.abs(minCenter)) / range;
79 
80             mPathMeasure.getPosTan(mYScrollProgress * mPathLength, mPathPoints, mPathTangent);
81 
82             boolean topClusterRisk =
83                     Math.abs(mPathPoints[1] - mCurveBottom) < EPSILON
84                             && minCenter < mPathPoints[1];
85             boolean bottomClusterRisk =
86                     Math.abs(mPathPoints[1] - mCurveTop) < EPSILON
87                             && maxCenter > mPathPoints[1];
88             // Continue offsetting the child along the straight-line part of the curve, if it
89             // has not gone off the screen when it reached the end of the original curve.
90             if (topClusterRisk || bottomClusterRisk) {
91                 mPathPoints[1] = verticalAnchor;
92                 mPathPoints[0] = (Math.abs(verticalAnchor) * mLineGradient);
93             }
94 
95             // Offset the View to match the provided anchor point.
96             int newLeft = (int) (mPathPoints[0] - mAnchorOffsetXY[0]);
97             child.offsetLeftAndRight(newLeft - child.getLeft());
98             float verticalTranslation = mPathPoints[1] - verticalAnchor;
99             child.setTranslationY(verticalTranslation);
100         } else {
101             child.setTranslationY(0);
102         }
103     }
104 
105     /**
106      * Override this method if you wish to adjust the anchor coordinates for each child view
107      * during a layout pass. In the override set the new desired anchor coordinates in
108      * the provided array. The coordinates should be provided in relation to the child view.
109      *
110      * @param child          The child view to which the anchor coordinates will apply.
111      * @param anchorOffsetXY The anchor coordinates for the provided child view, by default set
112      *                       to a pre-defined constant on the horizontal axis and half of the
113      *                       child height on the vertical axis (vertical center).
114      */
115     public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) {
116     }
117 
118     @VisibleForTesting
119     void setRound(boolean isScreenRound) {
120         mIsScreenRound = isScreenRound;
121     }
122 
123     @VisibleForTesting
124     void setOffset(int offset) {
125         mXCurveOffset = offset;
126     }
127 
128     /** Set up the initial layout for round screens. */
129     private void maybeSetUpCircularInitialLayout(int width, int height) {
130         // The values in this function are custom to the curve we use.
131         if (mCurvePathHeight != height) {
132             mCurvePathHeight = height;
133             mCurveBottom = -0.048f * height;
134             mCurveTop = 1.048f * height;
135             mLineGradient = 0.5f / 0.048f;
136             mCurvePath.reset();
137             mCurvePath.moveTo(0.5f * width, mCurveBottom);
138             mCurvePath.lineTo(0.34f * width, 0.075f * height);
139             mCurvePath.cubicTo(
140                     0.22f * width, 0.17f * height, 0.13f * width, 0.32f * height, 0.13f * width,
141                     height / 2);
142             mCurvePath.cubicTo(
143                     0.13f * width,
144                     0.68f * height,
145                     0.22f * width,
146                     0.83f * height,
147                     0.34f * width,
148                     0.925f * height);
149             mCurvePath.lineTo(width / 2, mCurveTop);
150             mPathMeasure.setPath(mCurvePath, false);
151             mPathLength = mPathMeasure.getLength();
152         }
153     }
154 }
155