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.animation.Animator; 20import android.animation.AnimatorSet; 21import android.animation.ObjectAnimator; 22import android.animation.StateListAnimator; 23import android.content.res.ColorStateList; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.graphics.drawable.GradientDrawable; 28import android.graphics.drawable.InsetDrawable; 29import android.graphics.drawable.LayerDrawable; 30import android.graphics.drawable.RippleDrawable; 31import android.os.Build; 32import android.support.annotation.RequiresApi; 33import android.support.v4.graphics.drawable.DrawableCompat; 34import android.view.View; 35 36import java.util.ArrayList; 37import java.util.List; 38 39@RequiresApi(21) 40class FloatingActionButtonLollipop extends FloatingActionButtonImpl { 41 42 private InsetDrawable mInsetDrawable; 43 44 FloatingActionButtonLollipop(VisibilityAwareImageButton view, 45 ShadowViewDelegate shadowViewDelegate) { 46 super(view, shadowViewDelegate); 47 } 48 49 @Override 50 void setBackgroundDrawable(ColorStateList backgroundTint, 51 PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) { 52 // Now we need to tint the shape background with the tint 53 mShapeDrawable = DrawableCompat.wrap(createShapeDrawable()); 54 DrawableCompat.setTintList(mShapeDrawable, backgroundTint); 55 if (backgroundTintMode != null) { 56 DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode); 57 } 58 59 final Drawable rippleContent; 60 if (borderWidth > 0) { 61 mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint); 62 rippleContent = new LayerDrawable(new Drawable[]{mBorderDrawable, mShapeDrawable}); 63 } else { 64 mBorderDrawable = null; 65 rippleContent = mShapeDrawable; 66 } 67 68 mRippleDrawable = new RippleDrawable(ColorStateList.valueOf(rippleColor), 69 rippleContent, null); 70 71 mContentBackground = mRippleDrawable; 72 73 mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable); 74 } 75 76 @Override 77 void setRippleColor(int rippleColor) { 78 if (mRippleDrawable instanceof RippleDrawable) { 79 ((RippleDrawable) mRippleDrawable).setColor(ColorStateList.valueOf(rippleColor)); 80 } else { 81 super.setRippleColor(rippleColor); 82 } 83 } 84 85 @Override 86 void onElevationsChanged(final float elevation, final float pressedTranslationZ) { 87 if (Build.VERSION.SDK_INT == 21) { 88 // Animations produce NPE in version 21. Bluntly set the values instead (matching the 89 // logic in the animations below). 90 if (mView.isEnabled()) { 91 mView.setElevation(elevation); 92 if (mView.isFocused() || mView.isPressed()) { 93 mView.setTranslationZ(pressedTranslationZ); 94 } else { 95 mView.setTranslationZ(0); 96 } 97 } else { 98 mView.setElevation(0); 99 mView.setTranslationZ(0); 100 } 101 } else { 102 final StateListAnimator stateListAnimator = new StateListAnimator(); 103 104 // Animate elevation and translationZ to our values when pressed 105 AnimatorSet set = new AnimatorSet(); 106 set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0)) 107 .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ) 108 .setDuration(PRESSED_ANIM_DURATION)); 109 set.setInterpolator(ANIM_INTERPOLATOR); 110 stateListAnimator.addState(PRESSED_ENABLED_STATE_SET, set); 111 112 // Same deal for when we're focused 113 set = new AnimatorSet(); 114 set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0)) 115 .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ) 116 .setDuration(PRESSED_ANIM_DURATION)); 117 set.setInterpolator(ANIM_INTERPOLATOR); 118 stateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, set); 119 120 // Animate translationZ to 0 if not pressed 121 set = new AnimatorSet(); 122 List<Animator> animators = new ArrayList<>(); 123 animators.add(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0)); 124 if (Build.VERSION.SDK_INT >= 22 && Build.VERSION.SDK_INT <= 24) { 125 // This is a no-op animation which exists here only for introducing the duration 126 // because setting the delay (on the next animation) via "setDelay" or "after" 127 // can trigger a NPE between android versions 22 and 24 (due to a framework 128 // bug). The issue has been fixed in version 25. 129 animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 130 mView.getTranslationZ()).setDuration(PRESSED_ANIM_DELAY)); 131 } 132 animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f) 133 .setDuration(PRESSED_ANIM_DURATION)); 134 set.playSequentially(animators.toArray(new ObjectAnimator[0])); 135 set.setInterpolator(ANIM_INTERPOLATOR); 136 stateListAnimator.addState(ENABLED_STATE_SET, set); 137 138 // Animate everything to 0 when disabled 139 set = new AnimatorSet(); 140 set.play(ObjectAnimator.ofFloat(mView, "elevation", 0f).setDuration(0)) 141 .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f).setDuration(0)); 142 set.setInterpolator(ANIM_INTERPOLATOR); 143 stateListAnimator.addState(EMPTY_STATE_SET, set); 144 145 mView.setStateListAnimator(stateListAnimator); 146 } 147 148 if (mShadowViewDelegate.isCompatPaddingEnabled()) { 149 updatePadding(); 150 } 151 } 152 153 @Override 154 public float getElevation() { 155 return mView.getElevation(); 156 } 157 158 @Override 159 void onCompatShadowChanged() { 160 updatePadding(); 161 } 162 163 @Override 164 void onPaddingUpdated(Rect padding) { 165 if (mShadowViewDelegate.isCompatPaddingEnabled()) { 166 mInsetDrawable = new InsetDrawable(mRippleDrawable, 167 padding.left, padding.top, padding.right, padding.bottom); 168 mShadowViewDelegate.setBackgroundDrawable(mInsetDrawable); 169 } else { 170 mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable); 171 } 172 } 173 174 @Override 175 void onDrawableStateChanged(int[] state) { 176 // no-op 177 } 178 179 @Override 180 void jumpDrawableToCurrentState() { 181 // no-op 182 } 183 184 @Override 185 boolean requirePreDrawListener() { 186 return false; 187 } 188 189 @Override 190 CircularBorderDrawable newCircularDrawable() { 191 return new CircularBorderDrawableLollipop(); 192 } 193 194 @Override 195 GradientDrawable newGradientDrawableForShape() { 196 return new AlwaysStatefulGradientDrawable(); 197 } 198 199 @Override 200 void getPadding(Rect rect) { 201 if (mShadowViewDelegate.isCompatPaddingEnabled()) { 202 final float radius = mShadowViewDelegate.getRadius(); 203 final float maxShadowSize = getElevation() + mPressedTranslationZ; 204 final int hPadding = (int) Math.ceil( 205 ShadowDrawableWrapper.calculateHorizontalPadding(maxShadowSize, radius, false)); 206 final int vPadding = (int) Math.ceil( 207 ShadowDrawableWrapper.calculateVerticalPadding(maxShadowSize, radius, false)); 208 rect.set(hPadding, vPadding, hPadding, vPadding); 209 } else { 210 rect.set(0, 0, 0, 0); 211 } 212 } 213 214 /** 215 * LayerDrawable on L+ caches its isStateful() state and doesn't refresh it, 216 * meaning that if we apply a tint to one of its children, the parent doesn't become 217 * stateful and the tint doesn't work for state changes. We workaround it by saying that we 218 * are always stateful. If we don't have a stateful tint, the change is ignored anyway. 219 */ 220 static class AlwaysStatefulGradientDrawable extends GradientDrawable { 221 @Override 222 public boolean isStateful() { 223 return true; 224 } 225 } 226} 227