1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.views;
17 
18 import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
19 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
20 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
21 
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.util.AttributeSet;
25 import android.util.SparseBooleanArray;
26 import android.view.View;
27 import android.widget.EdgeEffect;
28 import android.widget.RelativeLayout;
29 
30 import androidx.annotation.NonNull;
31 import androidx.dynamicanimation.animation.DynamicAnimation;
32 import androidx.dynamicanimation.animation.FloatPropertyCompat;
33 import androidx.dynamicanimation.animation.SpringAnimation;
34 import androidx.dynamicanimation.animation.SpringForce;
35 import androidx.recyclerview.widget.RecyclerView;
36 import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory;
37 
38 public class SpringRelativeLayout extends RelativeLayout {
39 
40     private static final float STIFFNESS = (STIFFNESS_MEDIUM + STIFFNESS_LOW) / 2;
41     private static final float DAMPING_RATIO = DAMPING_RATIO_MEDIUM_BOUNCY;
42     private static final float VELOCITY_MULTIPLIER = 0.3f;
43 
44     private static final FloatPropertyCompat<SpringRelativeLayout> DAMPED_SCROLL =
45             new FloatPropertyCompat<SpringRelativeLayout>("value") {
46 
47                 @Override
48                 public float getValue(SpringRelativeLayout object) {
49                     return object.mDampedScrollShift;
50                 }
51 
52                 @Override
53                 public void setValue(SpringRelativeLayout object, float value) {
54                     object.setDampedScrollShift(value);
55                 }
56             };
57 
58     protected final SparseBooleanArray mSpringViews = new SparseBooleanArray();
59     private final SpringAnimation mSpring;
60 
61     private float mDampedScrollShift = 0;
62     private SpringEdgeEffect mActiveEdge;
63 
SpringRelativeLayout(Context context)64     public SpringRelativeLayout(Context context) {
65         this(context, null);
66     }
67 
SpringRelativeLayout(Context context, AttributeSet attrs)68     public SpringRelativeLayout(Context context, AttributeSet attrs) {
69         this(context, attrs, 0);
70     }
71 
SpringRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr)72     public SpringRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
73         super(context, attrs, defStyleAttr);
74         mSpring = new SpringAnimation(this, DAMPED_SCROLL, 0);
75         mSpring.setSpring(new SpringForce(0)
76                 .setStiffness(STIFFNESS)
77                 .setDampingRatio(DAMPING_RATIO));
78     }
79 
addSpringView(int id)80     public void addSpringView(int id) {
81         mSpringViews.put(id, true);
82     }
83 
removeSpringView(int id)84     public void removeSpringView(int id) {
85         mSpringViews.delete(id);
86         invalidate();
87     }
88 
89     /**
90      * Used to clip the canvas when drawing child views during overscroll.
91      */
getCanvasClipTopForOverscroll()92     public int getCanvasClipTopForOverscroll() {
93         return 0;
94     }
95 
96     @Override
drawChild(Canvas canvas, View child, long drawingTime)97     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
98         if (mDampedScrollShift != 0 && mSpringViews.get(child.getId())) {
99             int saveCount = canvas.save();
100 
101             canvas.clipRect(0, getCanvasClipTopForOverscroll(), getWidth(), getHeight());
102             canvas.translate(0, mDampedScrollShift);
103             boolean result = super.drawChild(canvas, child, drawingTime);
104 
105             canvas.restoreToCount(saveCount);
106 
107             return result;
108         }
109         return super.drawChild(canvas, child, drawingTime);
110     }
111 
setActiveEdge(SpringEdgeEffect edge)112     private void setActiveEdge(SpringEdgeEffect edge) {
113         if (mActiveEdge != edge && mActiveEdge != null) {
114             mActiveEdge.mDistance = 0;
115         }
116         mActiveEdge = edge;
117     }
118 
setDampedScrollShift(float shift)119     protected void setDampedScrollShift(float shift) {
120         if (shift != mDampedScrollShift) {
121             mDampedScrollShift = shift;
122             invalidate();
123         }
124     }
125 
finishScrollWithVelocity(float velocity)126     private void finishScrollWithVelocity(float velocity) {
127         mSpring.setStartVelocity(velocity);
128         mSpring.setStartValue(mDampedScrollShift);
129         mSpring.start();
130     }
131 
finishWithShiftAndVelocity(float shift, float velocity, DynamicAnimation.OnAnimationEndListener listener)132     protected void finishWithShiftAndVelocity(float shift, float velocity,
133             DynamicAnimation.OnAnimationEndListener listener) {
134         setDampedScrollShift(shift);
135         mSpring.addEndListener(listener);
136         finishScrollWithVelocity(velocity);
137     }
138 
createEdgeEffectFactory()139     public EdgeEffectFactory createEdgeEffectFactory() {
140         return new SpringEdgeEffectFactory();
141     }
142 
143     private class SpringEdgeEffectFactory extends EdgeEffectFactory {
144 
145         @NonNull @Override
createEdgeEffect(RecyclerView view, int direction)146         protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
147             switch (direction) {
148                 case DIRECTION_TOP:
149                     return new SpringEdgeEffect(getContext(), +VELOCITY_MULTIPLIER);
150                 case DIRECTION_BOTTOM:
151                     return new SpringEdgeEffect(getContext(), -VELOCITY_MULTIPLIER);
152             }
153             return super.createEdgeEffect(view, direction);
154         }
155     }
156 
157     private class SpringEdgeEffect extends EdgeEffect {
158 
159         private final float mVelocityMultiplier;
160 
161         private float mDistance;
162 
SpringEdgeEffect(Context context, float velocityMultiplier)163         public SpringEdgeEffect(Context context, float velocityMultiplier) {
164             super(context);
165             mVelocityMultiplier = velocityMultiplier;
166         }
167 
168         @Override
draw(Canvas canvas)169         public boolean draw(Canvas canvas) {
170             return false;
171         }
172 
173         @Override
onAbsorb(int velocity)174         public void onAbsorb(int velocity) {
175             finishScrollWithVelocity(velocity * mVelocityMultiplier);
176         }
177 
178         @Override
onPull(float deltaDistance, float displacement)179         public void onPull(float deltaDistance, float displacement) {
180             setActiveEdge(this);
181             mDistance += deltaDistance * (mVelocityMultiplier / 3f);
182             setDampedScrollShift(mDistance * getHeight());
183         }
184 
185         @Override
onRelease()186         public void onRelease() {
187             mDistance = 0;
188             finishScrollWithVelocity(0);
189         }
190     }
191 }