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 }