/* * Copyright (C) 2015 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 com.android.launcher3.util; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; /** * This class differs from the framework {@link android.widget.EdgeEffect}: * 1) It does not use PorterDuffXfermode * 2) The width to radius factor is smaller (0.5 instead of 0.75) */ public class LauncherEdgeEffect { // Time it will take the effect to fully recede in ms private static final int RECEDE_TIME = 600; // Time it will take before a pulled glow begins receding in ms private static final int PULL_TIME = 167; // Time it will take in ms for a pulled glow to decay to partial strength before release private static final int PULL_DECAY_TIME = 2000; private static final float MAX_ALPHA = 0.5f; private static final float MAX_GLOW_SCALE = 2.f; private static final float PULL_GLOW_BEGIN = 0.f; // Minimum velocity that will be absorbed private static final int MIN_VELOCITY = 100; // Maximum velocity, clamps at this value private static final int MAX_VELOCITY = 10000; private static final float EPSILON = 0.001f; private static final double ANGLE = Math.PI / 6; private static final float SIN = (float) Math.sin(ANGLE); private static final float COS = (float) Math.cos(ANGLE); private float mGlowAlpha; private float mGlowScaleY; private float mGlowAlphaStart; private float mGlowAlphaFinish; private float mGlowScaleYStart; private float mGlowScaleYFinish; private long mStartTime; private float mDuration; private final Interpolator mInterpolator; private static final int STATE_IDLE = 0; private static final int STATE_PULL = 1; private static final int STATE_ABSORB = 2; private static final int STATE_RECEDE = 3; private static final int STATE_PULL_DECAY = 4; private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; private static final int VELOCITY_GLOW_FACTOR = 6; private int mState = STATE_IDLE; private float mPullDistance; private final Rect mBounds = new Rect(); private final Paint mPaint = new Paint(); private float mRadius; private float mBaseGlowScale; private float mDisplacement = 0.5f; private float mTargetDisplacement = 0.5f; /** * Construct a new EdgeEffect with a theme appropriate for the provided context. */ public LauncherEdgeEffect() { mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mInterpolator = new DecelerateInterpolator(); } /** * Set the size of this edge effect in pixels. * * @param width Effect width in pixels * @param height Effect height in pixels */ public void setSize(int width, int height) { final float r = width * 0.5f / SIN; final float y = COS * r; final float h = r - y; final float or = height * 0.75f / SIN; final float oy = COS * or; final float oh = or - oy; mRadius = r; mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); } /** * Reports if this EdgeEffect's animation is finished. If this method returns false * after a call to {@link #draw(Canvas)} the host widget should schedule another * drawing pass to continue the animation. * * @return true if animation is finished, false if drawing should continue on the next frame. */ public boolean isFinished() { return mState == STATE_IDLE; } /** * Immediately finish the current animation. * After this call {@link #isFinished()} will return true. */ public void finish() { mState = STATE_IDLE; } /** * A view should call this when content is pulled away from an edge by the user. * This will update the state of the current visual effect and its associated animation. * The host view should always {@link android.view.View#invalidate()} after this * and draw the results accordingly. * *
Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement * of the pull point is known.
* * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to * 1.f (full length of the view) or negative values to express change * back toward the edge reached to initiate the effect. */ public void onPull(float deltaDistance) { onPull(deltaDistance, 0.5f); } /** * A view should call this when content is pulled away from an edge by the user. * This will update the state of the current visual effect and its associated animation. * The host view should always {@link android.view.View#invalidate()} after this * and draw the results accordingly. * * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to * 1.f (full length of the view) or negative values to express change * back toward the edge reached to initiate the effect. * @param displacement The displacement from the starting side of the effect of the point * initiating the pull. In the case of touch this is the finger position. * Values may be from 0-1. */ public void onPull(float deltaDistance, float displacement) { final long now = AnimationUtils.currentAnimationTimeMillis(); mTargetDisplacement = displacement; if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { return; } if (mState != STATE_PULL) { mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); } mState = STATE_PULL; mStartTime = now; mDuration = PULL_TIME; mPullDistance += deltaDistance; final float absdd = Math.abs(deltaDistance); mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); if (mPullDistance == 0) { mGlowScaleY = mGlowScaleYStart = 0; } else { final float scale = (float) (Math.max(0, 1 - 1 / Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d); mGlowScaleY = mGlowScaleYStart = scale; } mGlowAlphaFinish = mGlowAlpha; mGlowScaleYFinish = mGlowScaleY; } /** * Call when the object is released after being pulled. * This will begin the "decay" phase of the effect. After calling this method * the host view should {@link android.view.View#invalidate()} and thereby * draw the results accordingly. */ public void onRelease() { mPullDistance = 0; if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { return; } mState = STATE_RECEDE; mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = RECEDE_TIME; } /** * Call when the effect absorbs an impact at the given velocity. * Used when a fling reaches the scroll boundary. * *When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
* the method getCurrVelocity
will provide a reasonable approximation
* to use here.