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 17package androidx.wear.widget; 18 19import android.content.Context; 20import android.graphics.Path; 21import android.graphics.PathMeasure; 22import android.view.View; 23 24import androidx.annotation.VisibleForTesting; 25import androidx.recyclerview.widget.RecyclerView; 26import 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 */ 32public 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 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 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 return; 117 } 118 119 @VisibleForTesting 120 void setRound(boolean isScreenRound) { 121 mIsScreenRound = isScreenRound; 122 } 123 124 @VisibleForTesting 125 void setOffset(int offset) { 126 mXCurveOffset = offset; 127 } 128 129 /** Set up the initial layout for round screens. */ 130 private void maybeSetUpCircularInitialLayout(int width, int height) { 131 // The values in this function are custom to the curve we use. 132 if (mCurvePathHeight != height) { 133 mCurvePathHeight = height; 134 mCurveBottom = -0.048f * height; 135 mCurveTop = 1.048f * height; 136 mLineGradient = 0.5f / 0.048f; 137 mCurvePath.reset(); 138 mCurvePath.moveTo(0.5f * width, mCurveBottom); 139 mCurvePath.lineTo(0.34f * width, 0.075f * height); 140 mCurvePath.cubicTo( 141 0.22f * width, 0.17f * height, 0.13f * width, 0.32f * height, 0.13f * width, 142 height / 2); 143 mCurvePath.cubicTo( 144 0.13f * width, 145 0.68f * height, 146 0.22f * width, 147 0.83f * height, 148 0.34f * width, 149 0.925f * height); 150 mCurvePath.lineTo(width / 2, mCurveTop); 151 mPathMeasure.setPath(mCurvePath, false); 152 mPathLength = mPathMeasure.getLength(); 153 } 154 } 155} 156