/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.wear.widget; import android.content.Context; import android.graphics.Path; import android.graphics.PathMeasure; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.support.wear.R; import android.view.View; /** * An implementation of the {@link WearableLinearLayoutManager.LayoutCallback} aligning the children * of the associated {@link WearableRecyclerView} along a pre-defined vertical curve. */ public class CurvingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback { private static final float EPSILON = 0.001f; private final Path mCurvePath; private final PathMeasure mPathMeasure; private int mCurvePathHeight; private int mXCurveOffset; private float mPathLength; private float mCurveBottom; private float mCurveTop; private float mLineGradient; private final float[] mPathPoints = new float[2]; private final float[] mPathTangent = new float[2]; private final float[] mAnchorOffsetXY = new float[2]; private RecyclerView mParentView; private boolean mIsScreenRound; private int mLayoutWidth; private int mLayoutHeight; public CurvingLayoutCallback(Context context) { mCurvePath = new Path(); mPathMeasure = new PathMeasure(); mIsScreenRound = context.getResources().getConfiguration().isScreenRound(); mXCurveOffset = context.getResources().getDimensionPixelSize( R.dimen.wrv_curve_default_x_offset); } @Override public void onLayoutFinished(View child, RecyclerView parent) { if (mParentView != parent || (mParentView != null && ( mParentView.getWidth() != parent.getWidth() || mParentView.getHeight() != parent.getHeight()))) { mParentView = parent; mLayoutWidth = mParentView.getWidth(); mLayoutHeight = mParentView.getHeight(); } if (mIsScreenRound) { maybeSetUpCircularInitialLayout(mLayoutWidth, mLayoutHeight); mAnchorOffsetXY[0] = mXCurveOffset; mAnchorOffsetXY[1] = child.getHeight() / 2.0f; adjustAnchorOffsetXY(child, mAnchorOffsetXY); float minCenter = -(float) child.getHeight() / 2; float maxCenter = mLayoutHeight + (float) child.getHeight() / 2; float range = maxCenter - minCenter; float verticalAnchor = (float) child.getTop() + mAnchorOffsetXY[1]; float mYScrollProgress = (verticalAnchor + Math.abs(minCenter)) / range; mPathMeasure.getPosTan(mYScrollProgress * mPathLength, mPathPoints, mPathTangent); boolean topClusterRisk = Math.abs(mPathPoints[1] - mCurveBottom) < EPSILON && minCenter < mPathPoints[1]; boolean bottomClusterRisk = Math.abs(mPathPoints[1] - mCurveTop) < EPSILON && maxCenter > mPathPoints[1]; // Continue offsetting the child along the straight-line part of the curve, if it // has not gone off the screen when it reached the end of the original curve. if (topClusterRisk || bottomClusterRisk) { mPathPoints[1] = verticalAnchor; mPathPoints[0] = (Math.abs(verticalAnchor) * mLineGradient); } // Offset the View to match the provided anchor point. int newLeft = (int) (mPathPoints[0] - mAnchorOffsetXY[0]); child.offsetLeftAndRight(newLeft - child.getLeft()); float verticalTranslation = mPathPoints[1] - verticalAnchor; child.setTranslationY(verticalTranslation); } else { child.setTranslationY(0); } } /** * Override this method if you wish to adjust the anchor coordinates for each child view * during a layout pass. In the override set the new desired anchor coordinates in * the provided array. The coordinates should be provided in relation to the child view. * * @param child The child view to which the anchor coordinates will apply. * @param anchorOffsetXY The anchor coordinates for the provided child view, by default set * to a pre-defined constant on the horizontal axis and half of the * child height on the vertical axis (vertical center). */ public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) { return; }; @VisibleForTesting void setRound(boolean isScreenRound) { mIsScreenRound = isScreenRound; } @VisibleForTesting void setOffset(int offset) { mXCurveOffset = offset; } /** Set up the initial layout for round screens. */ private void maybeSetUpCircularInitialLayout(int width, int height) { // The values in this function are custom to the curve we use. if (mCurvePathHeight != height) { mCurvePathHeight = height; mCurveBottom = -0.048f * height; mCurveTop = 1.048f * height; mLineGradient = 0.5f / 0.048f; mCurvePath.reset(); mCurvePath.moveTo(0.5f * width, mCurveBottom); mCurvePath.lineTo(0.34f * width, 0.075f * height); mCurvePath.cubicTo( 0.22f * width, 0.17f * height, 0.13f * width, 0.32f * height, 0.13f * width, height / 2); mCurvePath.cubicTo( 0.13f * width, 0.68f * height, 0.22f * width, 0.83f * height, 0.34f * width, 0.925f * height); mCurvePath.lineTo(width / 2, mCurveTop); mPathMeasure.setPath(mCurvePath, false); mPathLength = mPathMeasure.getLength(); } } }