[go: nahoru, domu]

blob: 8155f62da3c1957ca95ebba856ba8a8f10102b0b [file] [log] [blame]
/*
* Copyright (C) 2016 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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.VelocityTracker;
/**
* Class adding circular scrolling support to {@link WearableRecyclerView}.
*
* @hide
*/
@TargetApi(Build.VERSION_CODES.M)
@RestrictTo(LIBRARY_GROUP)
class ScrollManager {
// One second in milliseconds.
private static final int ONE_SEC_IN_MS = 1000;
private static final float VELOCITY_MULTIPLIER = 1.5f;
private static final float FLING_EDGE_RATIO = 1.5f;
/**
* Taps beyond this radius fraction are considered close enough to the bezel to be candidates
* for circular scrolling.
*/
private float mMinRadiusFraction = 0.0f;
private float mMinRadiusFractionSquared = mMinRadiusFraction * mMinRadiusFraction;
/** How many degrees you have to drag along the bezel to scroll one screen height. */
private float mScrollDegreesPerScreen = 180;
private float mScrollRadiansPerScreen = (float) Math.toRadians(mScrollDegreesPerScreen);
/** Radius of screen in pixels, ignoring insets, if any. */
private float mScreenRadiusPx;
private float mScreenRadiusPxSquared;
/** How many pixels to scroll for each radian of bezel scrolling. */
private float mScrollPixelsPerRadian;
/** Whether an {@link MotionEvent#ACTION_DOWN} was received near the bezel. */
private boolean mDown;
/**
* Whether the user tapped near the bezel and dragged approximately tangentially to initiate
* bezel scrolling.
*/
private boolean mScrolling;
/**
* The angle of the user's finger relative to the center of the screen for the last {@link
* MotionEvent} during bezel scrolling.
*/
private float mLastAngleRadians;
private RecyclerView mRecyclerView;
VelocityTracker mVelocityTracker;
/** Should be called after the window is attached to the view. */
void setRecyclerView(RecyclerView recyclerView, int width, int height) {
mRecyclerView = recyclerView;
mScreenRadiusPx = Math.max(width, height) / 2f;
mScreenRadiusPxSquared = mScreenRadiusPx * mScreenRadiusPx;
mScrollPixelsPerRadian = height / mScrollRadiansPerScreen;
mVelocityTracker = VelocityTracker.obtain();
}
/** Remove the binding with a {@link RecyclerView} */
void clearRecyclerView() {
mRecyclerView = null;
}
/**
* Method dealing with touch events intercepted from the attached {@link RecyclerView}.
*
* @param event the intercepted touch event.
* @return true if the even was handled, false otherwise.
*/
boolean onTouchEvent(MotionEvent event) {
float deltaX = event.getRawX() - mScreenRadiusPx;
float deltaY = event.getRawY() - mScreenRadiusPx;
float radiusSquared = deltaX * deltaX + deltaY * deltaY;
final MotionEvent vtev = MotionEvent.obtain(event);
mVelocityTracker.addMovement(vtev);
vtev.recycle();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (radiusSquared / mScreenRadiusPxSquared > mMinRadiusFractionSquared) {
mDown = true;
return true; // Consume the event.
}
break;
case MotionEvent.ACTION_MOVE:
if (mScrolling) {
float angleRadians = (float) Math.atan2(deltaY, deltaX);
float deltaRadians = angleRadians - mLastAngleRadians;
deltaRadians = normalizeAngleRadians(deltaRadians);
int scrollPixels = Math.round(deltaRadians * mScrollPixelsPerRadian);
if (scrollPixels != 0) {
mRecyclerView.scrollBy(0 /* x */, scrollPixels /* y */);
// Recompute deltaRadians in terms of rounded scrollPixels.
deltaRadians = scrollPixels / mScrollPixelsPerRadian;
mLastAngleRadians += deltaRadians;
mLastAngleRadians = normalizeAngleRadians(mLastAngleRadians);
}
// Always consume the event so that we never break the circular scrolling
// gesture.
return true;
}
if (mDown) {
float deltaXFromCenter = event.getRawX() - mScreenRadiusPx;
float deltaYFromCenter = event.getRawY() - mScreenRadiusPx;
float distFromCenter = (float) Math.hypot(deltaXFromCenter, deltaYFromCenter);
if (distFromCenter != 0) {
deltaXFromCenter /= distFromCenter;
deltaYFromCenter /= distFromCenter;
mScrolling = true;
mRecyclerView.invalidate();
mLastAngleRadians = (float) Math.atan2(deltaYFromCenter, deltaXFromCenter);
return true; // Consume the event.
}
} else {
// Double check we're not missing an event we should really be handling.
if (radiusSquared / mScreenRadiusPxSquared > mMinRadiusFractionSquared) {
mDown = true;
return true; // Consume the event.
}
}
break;
case MotionEvent.ACTION_UP:
mDown = false;
mScrolling = false;
mVelocityTracker.computeCurrentVelocity(ONE_SEC_IN_MS,
mRecyclerView.getMaxFlingVelocity());
int velocityY = (int) mVelocityTracker.getYVelocity();
if (event.getX() < FLING_EDGE_RATIO * mScreenRadiusPx) {
velocityY = -velocityY;
}
mVelocityTracker.clear();
if (Math.abs(velocityY) > mRecyclerView.getMinFlingVelocity()) {
return mRecyclerView.fling(0, (int) (VELOCITY_MULTIPLIER * velocityY));
}
break;
case MotionEvent.ACTION_CANCEL:
if (mDown) {
mDown = false;
mScrolling = false;
mRecyclerView.invalidate();
return true; // Consume the event.
}
break;
}
return false;
}
/**
* Normalizes an angle to be in the range [-pi, pi] by adding or subtracting 2*pi if necessary.
*
* @param angleRadians an angle in radians. Must be no more than 2*pi out of normal range.
* @return an angle in radians in the range [-pi, pi]
*/
private static float normalizeAngleRadians(float angleRadians) {
if (angleRadians < -Math.PI) {
angleRadians = (float) (angleRadians + Math.PI * 2);
}
if (angleRadians > Math.PI) {
angleRadians = (float) (angleRadians - Math.PI * 2);
}
return angleRadians;
}
/**
* Set how many degrees you have to drag along the bezel to scroll one screen height.
*
* @param degreesPerScreen desired degrees per screen scroll.
*/
public void setScrollDegreesPerScreen(float degreesPerScreen) {
mScrollDegreesPerScreen = degreesPerScreen;
mScrollRadiansPerScreen = (float) Math.toRadians(mScrollDegreesPerScreen);
}
/**
* Sets the width of a virtual 'bezel' close to the edge of the screen within which taps can be
* recognized as belonging to a rotary scrolling gesture.
*
* @param fraction desired fraction of the width of the screen to be treated as a valid rotary
* scrolling target.
*/
public void setBezelWidth(float fraction) {
mMinRadiusFraction = 1 - fraction;
mMinRadiusFractionSquared = mMinRadiusFraction * mMinRadiusFraction;
}
/**
* Returns how many degrees you have to drag along the bezel to scroll one screen height. See
* {@link #setScrollDegreesPerScreen(float)} for details.
*/
public float getScrollDegreesPerScreen() {
return mScrollDegreesPerScreen;
}
/**
* Returns the current bezel width for circular scrolling. See {@link #setBezelWidth(float)}
* for details.
*/
public float getBezelWidth() {
return 1 - mMinRadiusFraction;
}
}