CurvingLayoutCallback.java revision 9b028c55b13889bf75b5dd43b5b0f4051834ae1d
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 android.support.wear.widget; 18 19import android.content.Context; 20import android.graphics.Path; 21import android.graphics.PathMeasure; 22import android.support.annotation.VisibleForTesting; 23import android.support.v7.widget.RecyclerView; 24import android.support.wear.R; 25import android.view.View; 26 27/** 28 * An implementation of the {@link WearableLinearLayoutManager.LayoutCallback} aligning the children 29 * of the associated {@link WearableRecyclerView} along a pre-defined vertical curve. 30 */ 31public class CurvingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback { 32 private static final float EPSILON = 0.001f; 33 34 private final Path mCurvePath; 35 private final PathMeasure mPathMeasure; 36 private int mCurvePathHeight; 37 private int mXCurveOffset; 38 private float mPathLength; 39 private float mCurveBottom; 40 private float mCurveTop; 41 private float mLineGradient; 42 private final float[] mPathPoints = new float[2]; 43 private final float[] mPathTangent = new float[2]; 44 private final float[] mAnchorOffsetXY = new float[2]; 45 46 private RecyclerView mParentView; 47 private boolean mIsScreenRound; 48 private int mLayoutWidth; 49 private int mLayoutHeight; 50 51 public CurvingLayoutCallback(Context context) { 52 mCurvePath = new Path(); 53 mPathMeasure = new PathMeasure(); 54 mIsScreenRound = context.getResources().getConfiguration().isScreenRound(); 55 mXCurveOffset = context.getResources().getDimensionPixelSize( 56 R.dimen.wrv_curve_default_x_offset); 57 } 58 59 @Override 60 public void onLayoutFinished(View child, RecyclerView parent) { 61 if (mParentView != parent || (mParentView != null && ( 62 mParentView.getWidth() != parent.getWidth() 63 || mParentView.getHeight() != parent.getHeight()))) { 64 mParentView = parent; 65 mLayoutWidth = mParentView.getWidth(); 66 mLayoutHeight = mParentView.getHeight(); 67 } 68 if (mIsScreenRound) { 69 maybeSetUpCircularInitialLayout(mLayoutWidth, mLayoutHeight); 70 mAnchorOffsetXY[0] = mXCurveOffset; 71 mAnchorOffsetXY[1] = child.getHeight() / 2.0f; 72 adjustAnchorOffsetXY(child, mAnchorOffsetXY); 73 float minCenter = -(float) child.getHeight() / 2; 74 float maxCenter = mLayoutHeight + (float) child.getHeight() / 2; 75 float range = maxCenter - minCenter; 76 float verticalAnchor = (float) child.getTop() + mAnchorOffsetXY[1]; 77 float mYScrollProgress = (verticalAnchor + Math.abs(minCenter)) / range; 78 79 mPathMeasure.getPosTan(mYScrollProgress * mPathLength, mPathPoints, mPathTangent); 80 81 boolean topClusterRisk = 82 Math.abs(mPathPoints[1] - mCurveBottom) < EPSILON 83 && minCenter < mPathPoints[1]; 84 boolean bottomClusterRisk = 85 Math.abs(mPathPoints[1] - mCurveTop) < EPSILON 86 && maxCenter > mPathPoints[1]; 87 // Continue offsetting the child along the straight-line part of the curve, if it 88 // has not gone off the screen when it reached the end of the original curve. 89 if (topClusterRisk || bottomClusterRisk) { 90 mPathPoints[1] = verticalAnchor; 91 mPathPoints[0] = (Math.abs(verticalAnchor) * mLineGradient); 92 } 93 94 // Offset the View to match the provided anchor point. 95 int newLeft = (int) (mPathPoints[0] - mAnchorOffsetXY[0]); 96 child.offsetLeftAndRight(newLeft - child.getLeft()); 97 float verticalTranslation = mPathPoints[1] - verticalAnchor; 98 child.setTranslationY(verticalTranslation); 99 } else { 100 child.setTranslationY(0); 101 } 102 } 103 104 /** 105 * Override this method if you wish to adjust the anchor coordinates for each child view 106 * during a layout pass. In the override set the new desired anchor coordinates in 107 * the provided array. The coordinates should be provided in relation to the child view. 108 * 109 * @param child The child view to which the anchor coordinates will apply. 110 * @param anchorOffsetXY The anchor coordinates for the provided child view, by default set 111 * to a pre-defined constant on the horizontal axis and half of the 112 * child height on the vertical axis (vertical center). 113 */ 114 public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) { 115 return; 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