之前在实现控件阴影时有提到过,阴影效果的实现采用的是Android原生的View的属性,拔高Z轴。Z轴会让View产生阴影的效果。
Z=elevation+ translationZ
拔高Z轴可以通过控制elevation和translationZ。
我们之前是通过elevation来单纯的控制Z轴;而translateZ,除了控制Z轴,还可以用来控制动画效果,比如我们点击按钮时希望它有一个弹起的效果,就是借助这个属性来实现。
StateAnimatorView接口
创建一个通用接口:
public interface StateAnimatorView {
void setStateAnimator(StateAnimator stateAnimator);
StateAnimator getStateAnimator();
}
实现接口
ShapeButton 实现 StateAnimatorView接口:
// -------------------------------
// stateAnimator
// -------------------------------
private StateAnimator stateAnimator = new StateAnimator(this);
@Override
public void setStateAnimator(StateAnimator stateAnimator) {
this.stateAnimator = stateAnimator;
}
@Override
public StateAnimator getStateAnimator() {
return stateAnimator;
}
StateAnimator
StateAnimator是一个管理View状态和相关Animator的类:
public class StateAnimator {
private final ArrayList<Tuple> mTuples = new ArrayList<>();
private Tuple lastMatch = null;
private Animator runningAnimation = null;
private WeakReference<View> viewRef;
public StateAnimator(View target) {
setTarget(target);
}
private Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (runningAnimation == animation) {
runningAnimation = null;
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
/**
* Associates the given Animation with the provided drawable state specs so that it will be run
* when the View's drawable state matches the specs.
*
* @param specs drawable state specs to match against
* @param animation The Animation to run when the specs match
*/
public void addState(int[] specs, Animator animation, Animator.AnimatorListener listener) {
Tuple tuple = new Tuple(specs, animation, listener);
animation.addListener(mAnimationListener);
mTuples.add(tuple);
}
/**
* Returns the current {@link Animation} which is started because of a state change.
*
* @return The currently running Animation or null if no Animation is running
*/
Animator getRunningAnimation() {
return runningAnimation;
}
View getTarget() {
return viewRef == null ? null : viewRef.get();
}
void setTarget(View view) {
final View current = getTarget();
if (current == view) {
return;
}
if (current != null) {
clearTarget();
}
if (view != null) {
viewRef = new WeakReference<>(view);
}
}
private void clearTarget() {
viewRef = null;
lastMatch = null;
runningAnimation = null;
}
/**
* Called by View
*/
public void setState(int[] state) {
Tuple match = null;
final int count = mTuples.size();
for (int i = 0; i < count; i++) {
final Tuple tuple = mTuples.get(i);
if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
match = tuple;
break;
}
}
if (match == lastMatch) {
return;
}
if (lastMatch != null) {
cancel();
}
lastMatch = match;
View view = (View) viewRef.get();
if (match != null && view != null && view.getVisibility() == View.VISIBLE) {
start(match);
}
}
private void start(Tuple match) {
match.getListener().onAnimationStart(match.animation);
runningAnimation = match.animation;
runningAnimation.start();
}
private void cancel() {
if (runningAnimation != null && runningAnimation.isRunning()) {
runningAnimation.cancel();
runningAnimation = null;
}
}
/**
* @hide
*/
ArrayList<Tuple> getTuples() {
return mTuples;
}
static class Tuple {
final int[] mSpecs;
final Animator animation;
private Animator.AnimatorListener listener;
private Tuple(int[] specs, Animator Animation, Animator.AnimatorListener listener) {
mSpecs = specs;
animation = Animation;
this.listener = listener;
}
int[] getSpecs() {
return mSpecs;
}
Animator getAnimation() {
return animation;
}
public Animator.AnimatorListener getListener() {
return listener;
}
}
}
在初始化阴影属性的地方,初始化StateAnimation:
public static void initElevation(ShadowView view, TypedArray a, int[] ids) {
int carbon_elevation = ids[0];
int carbon_shadowColor = ids[1];
int carbon_ambientShadowColor = ids[2];
int carbon_spotShadowColor = ids[3];
float elevation = a.getDimension(carbon_elevation, 0);
view.setElevation(elevation);
// 初始化StateAnimation
if (elevation > 0)
AnimUtils.setupElevationAnimator(((StateAnimatorView) view).getStateAnimator(), view);
ColorStateList shadowColor = a.getColorStateList(carbon_shadowColor);
view.setElevationShadowColor(shadowColor != null ? shadowColor.withAlpha(255) : null);
if (a.hasValue(carbon_ambientShadowColor)) {
ColorStateList ambientShadowColor = a.getColorStateList(carbon_ambientShadowColor);
view.setOutlineAmbientShadowColor(ambientShadowColor != null ? ambientShadowColor.withAlpha(255) : null);
}
if (a.hasValue(carbon_spotShadowColor)) {
ColorStateList spotShadowColor = a.getColorStateList(carbon_spotShadowColor);
view.setOutlineSpotShadowColor(spotShadowColor != null ? spotShadowColor.withAlpha(255) : null);
}
}
按下按钮和松开按钮的状态和动画是一一对应的:
public static void setupElevationAnimator(StateAnimator stateAnimator, final ShadowView view) {
// 按下时的状态和动画
{
final ValueAnimator animator = ValueAnimator.ofFloat(0, 0);
animator.setDuration(SHORT_ANIMATION_DURATION);
animator.setInterpolator(new FastOutSlowInInterpolator());
Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
float elevationOrTranslationZ = getElevationOrTranslationZ(view);
animator.setFloatValues(0, elevationOrTranslationZ);
}
};
animator.addUpdateListener(animation -> view.setTranslationZ((Float) animation.getAnimatedValue()));
stateAnimator.addState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}, animator, animatorListener);
}
// 松开时的状态和动画
{
final ValueAnimator animator = ValueAnimator.ofFloat(0, 0);
animator.setDuration(SHORT_ANIMATION_DURATION);
animator.setInterpolator(new FastOutSlowInInterpolator());
Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
float elevationOrTranslationZ = getElevationOrTranslationZ(view);
animator.setFloatValues(elevationOrTranslationZ, 0);
}
};
animator.addUpdateListener(animation -> view.setTranslationZ((Float) animation.getAnimatedValue()));
stateAnimator.addState(new int[]{-android.R.attr.state_pressed, android.R.attr.state_enabled}, animator, animatorListener);
}
// 松开时的状态和动画
{
final ValueAnimator animator = ValueAnimator.ofFloat(0, 0);
animator.setDuration(SHORT_ANIMATION_DURATION);
animator.setInterpolator(new FastOutSlowInInterpolator());
Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
animator.setFloatValues(view.getElevation(), 0);
}
};
animator.addUpdateListener(animation -> view.setTranslationZ((Float) animation.getAnimatedValue()));
stateAnimator.addState(new int[]{android.R.attr.state_enabled}, animator, animatorListener);
}
}
开始动画
在按下或松开按钮时,会回调按钮的drawableStateChanged():
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
if (stateAnimator != null)
// 这个方法会执行与状态相对应的动画
stateAnimator.setState(getDrawableState());
}
完整代码可查看:
ShapeButton部分