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