blob: 275f1f8b41af5bc3288c352c46cecd9fb05c7cc6 [file] [log] [blame]
/*
* 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.ws_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();
}
}
}