/* * Copyright (C) 2016 Thomas Robert Altstidl * * 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.tr4android.support.extension.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.support.v4.graphics.ColorUtils; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.TranslateAnimation; import android.widget.TextView; import com.tr4android.appcompat.extension.R; import com.tr4android.support.extension.animation.AnimationUtils; import com.tr4android.support.extension.drawable.RoundRectDrawable; import com.tr4android.support.extension.drawable.ShadowDrawableWrapper; import com.tr4android.support.extension.utils.ViewCompatUtils; /** * A TextView subclass that supports elevation and touch feedback pre-Lollipop * and has easy-to-use animations */ public class LabelView extends TextView { private static final int SHOW_HIDE_ANIM_DURATION = 200; private int mBackgroundColor; private int mRippleColor; private float mCornerRadius; private float mElevation; private boolean mIsHiding; private float mAnimationOffset; public LabelView(Context context) { this(context, null); } public LabelView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LabelView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray attr = context.obtainStyledAttributes( attrs, R.styleable.LabelView, defStyleAttr, 0); // Background values mBackgroundColor = attr.getColor(R.styleable.LabelView_labelBackgroundColor, 0); mRippleColor = attr.getColor(R.styleable.LabelView_labelRippleColor, 0); mCornerRadius = attr.getDimension(R.styleable.LabelView_labelCornerRadius, 0f); mElevation = attr.getDimension(R.styleable.LabelView_labelElevation, 0f); // Padding values int paddingHorizontal = attr.getDimensionPixelSize( R.styleable.LabelView_labelPaddingHorizontal, 0); int paddingVertical = attr.getDimensionPixelSize( R.styleable.LabelView_labelPaddingVertical, 0); attr.recycle(); setFocusable(true); setClickable(true); initBackground(); setCompatPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical); } /** * Sets the translation distance used for the show and hide animations * @param offset the offset for animations */ public void setAnimationOffset(float offset) { mAnimationOffset = offset; } /** * Gets the translation distance used for the show and hide animations * @return the offset for animations */ public float getAnimationOffset() { return mAnimationOffset; } /** * Compatibility method for properly setting padding * This takes into account the additional padding required by the shadow on pre-Lollipop devices */ public void setCompatPadding(int left, int top, int right, int bottom) { // Adjust padding by shadow final Rect shadowPadding = new Rect(); getBackground().getPadding(shadowPadding); setPadding(left + shadowPadding.left, top + shadowPadding.top, right + shadowPadding.right, bottom + shadowPadding.bottom); } /** * Hides the label using a translation animation */ public void hide() { if (mIsHiding || getVisibility() != VISIBLE) { // Already hiding or hidden, skip the call return; } if (!ViewCompat.isLaidOut(this)) { // View isn't laid out yet setVisibility(GONE); } // Use platform dependent animations if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { hideIceCreamSandwich(); } else { hideEclairMr1(); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void hideIceCreamSandwich() { animate().cancel(); animate().translationX(mAnimationOffset).alpha(0f) .setDuration(SHOW_HIDE_ANIM_DURATION) .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR) .setListener(new AnimatorListenerAdapter() { private boolean mCancelled; @Override public void onAnimationStart(Animator animation) { mIsHiding = true; } @Override public void onAnimationCancel(Animator animation) { mIsHiding = false; mCancelled = true; } @Override public void onAnimationEnd(Animator animation) { mIsHiding = false; // cancelled animations don't change visibility if (!mCancelled) { setVisibility(GONE); } } }); } private void hideEclairMr1() { Animation anim = createAnimationSet(1.0f, 0.0f, 0, Math.round(mAnimationOffset)); anim.setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR); anim.setDuration(SHOW_HIDE_ANIM_DURATION); anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() { @Override public void onAnimationStart(Animation animation) { mIsHiding = true; } @Override public void onAnimationEnd(Animation animation) { mIsHiding = false; setVisibility(GONE); } }); startAnimation(anim); } /** * Shows the label using a translation animation */ public void show() { if (!mIsHiding && getVisibility() == VISIBLE) { // Already showing or shown, skip the call return; } if (!ViewCompat.isLaidOut(this)) { // View isn't laid out yet return; } // Use platform dependent animations if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { showIceCreamSandwich(); } else { showEclairMr1(); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void showIceCreamSandwich() { animate().cancel(); if (getVisibility() != VISIBLE) { setTranslationX(mAnimationOffset); setAlpha(0f); } animate().translationX(0f).alpha(1f) .setDuration(SHOW_HIDE_ANIM_DURATION) .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { setVisibility(VISIBLE); } }); } private void showEclairMr1() { clearAnimation(); setVisibility(VISIBLE); Animation anim = createAnimationSet(0.0f, 1.0f, Math.round(mAnimationOffset), 0); anim.setDuration(SHOW_HIDE_ANIM_DURATION); anim.setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); startAnimation(anim); } private void initBackground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { initBackgroundLollipop(); } else { initBackgroundEclairMr1(); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void initBackgroundLollipop() { RoundRectDrawable shapeDrawable = new RoundRectDrawable(mBackgroundColor, mCornerRadius); RippleDrawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(mRippleColor), shapeDrawable, null); setElevation(mElevation); ViewCompatUtils.setBackground(this, rippleDrawable); } private void initBackgroundEclairMr1() { RoundRectDrawable shapeDrawable = new RoundRectDrawable(mBackgroundColor, mCornerRadius); shapeDrawable.setRoundRectTintList(createColorStateList(mRippleColor, mBackgroundColor)); // Shadow drawable that wraps the above drawable ShadowDrawableWrapper shadowDrawable = new ShadowDrawableWrapper( getResources(), shapeDrawable, mCornerRadius, mElevation, mElevation); shadowDrawable.setAddPaddingForCorners(false); ViewCompatUtils.setBackground(this, shadowDrawable); } // Helper that creates an AnimationSet with the specified translation private static Animation createAnimationSet(float fromAlpha, float toAlpha, int fromXDelta, int toXDelta) { AnimationSet anim = new AnimationSet(true); anim.addAnimation(new AlphaAnimation(fromAlpha, toAlpha)); anim.addAnimation(new TranslateAnimation(fromXDelta, toXDelta, 0, 0)); return anim; } // Helper that creates a ColorStateList with the selected ripple color private static ColorStateList createColorStateList(int rippleColor, int backgroundColor) { final int compositeColor = ColorUtils.compositeColors(rippleColor, backgroundColor); return new ColorStateList(new int[][]{ // states new int[]{android.R.attr.state_focused}, new int[]{android.R.attr.state_pressed}, new int[]{} // state_default }, new int[]{ // colors compositeColor, compositeColor, backgroundColor }); } }