1/* 2 * Copyright (C) 2015 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 17package android.support.design.widget; 18 19import android.content.res.ColorStateList; 20import android.graphics.Color; 21import android.graphics.PorterDuff; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.graphics.drawable.GradientDrawable; 25import android.graphics.drawable.LayerDrawable; 26import android.support.annotation.Nullable; 27import android.support.design.R; 28import android.support.design.widget.AnimationUtils.AnimationListenerAdapter; 29import android.support.v4.graphics.drawable.DrawableCompat; 30import android.view.View; 31import android.view.animation.Animation; 32import android.view.animation.Transformation; 33 34class FloatingActionButtonEclairMr1 extends FloatingActionButtonImpl { 35 36 private int mAnimationDuration; 37 private StateListAnimator mStateListAnimator; 38 private boolean mIsHiding; 39 40 ShadowDrawableWrapper mShadowDrawable; 41 42 FloatingActionButtonEclairMr1(VisibilityAwareImageButton view, 43 ShadowViewDelegate shadowViewDelegate) { 44 super(view, shadowViewDelegate); 45 46 mAnimationDuration = view.getResources().getInteger(android.R.integer.config_shortAnimTime); 47 48 mStateListAnimator = new StateListAnimator(); 49 mStateListAnimator.setTarget(view); 50 51 // Elevate with translationZ when pressed or focused 52 mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET, 53 setupAnimation(new ElevateToTranslationZAnimation())); 54 mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, 55 setupAnimation(new ElevateToTranslationZAnimation())); 56 // Reset back to elevation by default 57 mStateListAnimator.addState(EMPTY_STATE_SET, 58 setupAnimation(new ResetElevationAnimation())); 59 } 60 61 @Override 62 void setBackgroundDrawable(ColorStateList backgroundTint, 63 PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) { 64 // Now we need to tint the original background with the tint, using 65 // an InsetDrawable if we have a border width 66 mShapeDrawable = DrawableCompat.wrap(createShapeDrawable()); 67 DrawableCompat.setTintList(mShapeDrawable, backgroundTint); 68 if (backgroundTintMode != null) { 69 DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode); 70 } 71 72 // Now we created a mask Drawable which will be used for touch feedback. 73 GradientDrawable touchFeedbackShape = createShapeDrawable(); 74 75 // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need 76 // to inset for any border here as LayerDrawable will nest the padding for us 77 mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape); 78 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 79 80 final Drawable[] layers; 81 if (borderWidth > 0) { 82 mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint); 83 layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable}; 84 } else { 85 mBorderDrawable = null; 86 layers = new Drawable[] {mShapeDrawable, mRippleDrawable}; 87 } 88 89 mContentBackground = new LayerDrawable(layers); 90 91 mShadowDrawable = new ShadowDrawableWrapper( 92 mView.getResources(), 93 mContentBackground, 94 mShadowViewDelegate.getRadius(), 95 mElevation, 96 mElevation + mPressedTranslationZ); 97 mShadowDrawable.setAddPaddingForCorners(false); 98 mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable); 99 } 100 101 @Override 102 void setBackgroundTintList(ColorStateList tint) { 103 if (mShapeDrawable != null) { 104 DrawableCompat.setTintList(mShapeDrawable, tint); 105 } 106 if (mBorderDrawable != null) { 107 mBorderDrawable.setBorderTint(tint); 108 } 109 } 110 111 @Override 112 void setBackgroundTintMode(PorterDuff.Mode tintMode) { 113 if (mShapeDrawable != null) { 114 DrawableCompat.setTintMode(mShapeDrawable, tintMode); 115 } 116 } 117 118 @Override 119 void setRippleColor(int rippleColor) { 120 if (mRippleDrawable != null) { 121 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 122 } 123 } 124 125 @Override 126 float getElevation() { 127 return mElevation; 128 } 129 130 @Override 131 void onElevationChanged(float elevation) { 132 if (mShadowDrawable != null) { 133 mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ); 134 updatePadding(); 135 } 136 } 137 138 @Override 139 void onTranslationZChanged(float translationZ) { 140 if (mShadowDrawable != null) { 141 mShadowDrawable.setMaxShadowSize(mElevation + translationZ); 142 updatePadding(); 143 } 144 } 145 146 @Override 147 void onDrawableStateChanged(int[] state) { 148 mStateListAnimator.setState(state); 149 } 150 151 @Override 152 void jumpDrawableToCurrentState() { 153 mStateListAnimator.jumpToCurrentState(); 154 } 155 156 @Override 157 void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) { 158 if (mIsHiding || mView.getVisibility() != View.VISIBLE) { 159 // A hide animation is in progress, or we're already hidden. Skip the call 160 if (listener != null) { 161 listener.onHidden(); 162 } 163 return; 164 } 165 166 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 167 mView.getContext(), R.anim.design_fab_out); 168 anim.setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR); 169 anim.setDuration(SHOW_HIDE_ANIM_DURATION); 170 anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() { 171 @Override 172 public void onAnimationStart(Animation animation) { 173 mIsHiding = true; 174 } 175 176 @Override 177 public void onAnimationEnd(Animation animation) { 178 mIsHiding = false; 179 mView.internalSetVisibility(View.GONE, fromUser); 180 if (listener != null) { 181 listener.onHidden(); 182 } 183 } 184 }); 185 mView.startAnimation(anim); 186 } 187 188 @Override 189 void show(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) { 190 if (mView.getVisibility() != View.VISIBLE || mIsHiding) { 191 // If the view is not visible, or is visible and currently being hidden, run 192 // the show animation 193 mView.clearAnimation(); 194 mView.internalSetVisibility(View.VISIBLE, fromUser); 195 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 196 mView.getContext(), R.anim.design_fab_in); 197 anim.setDuration(SHOW_HIDE_ANIM_DURATION); 198 anim.setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 199 anim.setAnimationListener(new AnimationListenerAdapter() { 200 @Override 201 public void onAnimationEnd(Animation animation) { 202 if (listener != null) { 203 listener.onShown(); 204 } 205 } 206 }); 207 mView.startAnimation(anim); 208 } else { 209 if (listener != null) { 210 listener.onShown(); 211 } 212 } 213 } 214 215 @Override 216 void onCompatShadowChanged() { 217 // Ignore pre-v21 218 } 219 220 void getPadding(Rect rect) { 221 mShadowDrawable.getPadding(rect); 222 } 223 224 private Animation setupAnimation(Animation animation) { 225 animation.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 226 animation.setDuration(mAnimationDuration); 227 return animation; 228 } 229 230 private abstract class BaseShadowAnimation extends Animation { 231 private float mShadowSizeStart; 232 private float mShadowSizeDiff; 233 234 @Override 235 public void reset() { 236 super.reset(); 237 238 mShadowSizeStart = mShadowDrawable.getShadowSize(); 239 mShadowSizeDiff = getTargetShadowSize() - mShadowSizeStart; 240 } 241 242 @Override 243 protected void applyTransformation(float interpolatedTime, Transformation t) { 244 mShadowDrawable.setShadowSize(mShadowSizeStart + (mShadowSizeDiff * interpolatedTime)); 245 } 246 247 /** 248 * @return the shadow size we want to animate to. 249 */ 250 protected abstract float getTargetShadowSize(); 251 } 252 253 private class ResetElevationAnimation extends BaseShadowAnimation { 254 @Override 255 protected float getTargetShadowSize() { 256 return mElevation; 257 } 258 } 259 260 private class ElevateToTranslationZAnimation extends BaseShadowAnimation { 261 @Override 262 protected float getTargetShadowSize() { 263 return mElevation + mPressedTranslationZ; 264 } 265 } 266 267 private static ColorStateList createColorStateList(int selectedColor) { 268 final int[][] states = new int[3][]; 269 final int[] colors = new int[3]; 270 int i = 0; 271 272 states[i] = FOCUSED_ENABLED_STATE_SET; 273 colors[i] = selectedColor; 274 i++; 275 276 states[i] = PRESSED_ENABLED_STATE_SET; 277 colors[i] = selectedColor; 278 i++; 279 280 // Default enabled state 281 states[i] = new int[0]; 282 colors[i] = Color.TRANSPARENT; 283 i++; 284 285 return new ColorStateList(states, colors); 286 } 287}