android5.0以后,系统多了许多由手指拖拉同时产生的动画,而我们通常所使用的Animator播放出来都是一个完整的流程,很难拆分出来以适应手指的滑动.虽然可以使用View#ALPHA,View#
TRANSLATION_X等动画的属性值直接设置,但是一旦想让多个动画同时进行,比如平移+旋转+透明,代码逻辑会显得非常复杂.我在源码中找到了系统中使用的封装好的一个类
TouchAnimator.由于这个类是隐藏的,所以我的做法是将其拷贝出来,这样也比较好修改(如果有需求的话)
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;
import java.util.ArrayList;
import java.util.List;
public class TouchAnimator {
private final Object[] mTargets;
private final KeyframeSet[] mKeyframeSets;
private final float mStartDelay;
private final float mEndDelay;
private final float mSpan;
private final Interpolator mInterpolator;
private final Listener mListener;
private float mLastT = -1;
private TouchAnimator(Object[] targets, KeyframeSet[] keyframeSets,
float startDelay, float endDelay, Interpolator interpolator, Listener listener) {
mTargets = targets;
mKeyframeSets = keyframeSets;
mStartDelay = startDelay;
mEndDelay = endDelay;
mSpan = (1 - mEndDelay - mStartDelay);
mInterpolator = interpolator;
mListener = listener;
}
public void setPosition(float fraction) {
float t = MathUtils.constrain((fraction - mStartDelay) / mSpan, 0, 1);
if (mInterpolator != null) {
t = mInterpolator.getInterpolation(t);
}
if (t == mLastT) {
return;
}
if (mListener != null) {
if (t == 1) {
mListener.onAnimationAtEnd();
} else if (t == 0) {
mListener.onAnimationAtStart();
} else if (mLastT <= 0 || mLastT == 1) {
mListener.onAnimationStarted();
}
mLastT = t;
}
for (int i = 0; i < mTargets.length; i++) {
mKeyframeSets[i].setValue(t, mTargets[i]);
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
private static final FloatProperty<TouchAnimator> POSITION =
new FloatProperty<TouchAnimator>("position") {
@Override
public void setValue(TouchAnimator touchAnimator, float value) {
touchAnimator.setPosition(value);
}
@Override
public Float get(TouchAnimator touchAnimator) {
return touchAnimator.mLastT;
}
};
public static class ListenerAdapter implements Listener {
@Override
public void onAnimationAtStart() {
}
@Override
public void onAnimationAtEnd() {
}
@Override
public void onAnimationStarted() {
}
}
public interface Listener {
/**
* Called when the animator moves into a position of "0". Start and end delays are
* taken into account, so this position may cover a range of fractional inputs.
*/
void onAnimationAtStart();
/**
* Called when the animator moves into a position of "0". Start and end delays are
* taken into account, so this position may cover a range of fractional inputs.
*/
void onAnimationAtEnd();
/**
* Called when the animator moves out of the start or end position and is in a transient
* state.
*/
void onAnimationStarted();
}
public static class Builder {
private List<Object> mTargets = new ArrayList<>();
private List<KeyframeSet> mValues = new ArrayList<>();
private float mStartDelay;
private float mEndDelay;
private Interpolator mInterpolator;
private Listener mListener;
public Builder addFloat(Object target, String property, float... values) {
add(target, KeyframeSet.ofFloat(getProperty(target, property, float.class), values));
return this;
}
public Builder addInt(Object target, String property, int... values) {
add(target, KeyframeSet.ofInt(getProperty(target, property, int.class), values));
return this;
}
private void add(Object target, KeyframeSet keyframeSet) {
mTargets.add(target);
mValues.add(keyframeSet);
}
private static Property getProperty(Object target, String property, Class<?> cls) {
if (target instanceof View) {
switch (property) {
case "translationX":
return View.TRANSLATION_X;
case "translationY":
return View.TRANSLATION_Y;
case "translationZ":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return View.TRANSLATION_Z;
}
return null;
case "alpha":
return View.ALPHA;
case "rotation":
return View.ROTATION;
case "x":
return View.X;
case "y":
return View.Y;
case "scaleX":
return View.SCALE_X;
case "scaleY":
return View.SCALE_Y;
}
}
if (target instanceof TouchAnimator && "position".equals(property)) {
return POSITION;
}
return Property.of(target.getClass(), cls, property);
}
public Builder setStartDelay(float startDelay) {
mStartDelay = startDelay;
return this;
}
public Builder setEndDelay(float endDelay) {
mEndDelay = endDelay;
return this;
}
public Builder setInterpolator(Interpolator intepolator) {
mInterpolator = intepolator;
return this;
}
public Builder setListener(Listener listener) {
mListener = listener;
return this;
}
public TouchAnimator build() {
return new TouchAnimator(mTargets.toArray(new Object[mTargets.size()]),
mValues.toArray(new KeyframeSet[mValues.size()]),
mStartDelay, mEndDelay, mInterpolator, mListener);
}
}
private static abstract class KeyframeSet {
private final float mFrameWidth;
private final int mSize;
public KeyframeSet(int size) {
mSize = size;
mFrameWidth = 1 / (float) (size - 1);
}
void setValue(float fraction, Object target) {
int i;
for (i = 1; i < mSize - 1 && fraction > mFrameWidth; i++) ;
float amount = fraction / mFrameWidth;
interpolate(i, amount, target);
}
protected abstract void interpolate(int index, float amount, Object target);
public static KeyframeSet ofInt(Property property, int... values) {
return new IntKeyframeSet((Property<?, Integer>) property, values);
}
public static KeyframeSet ofFloat(Property property, float... values) {
return new FloatKeyframeSet((Property<?, Float>) property, values);
}
}
private static class FloatKeyframeSet<T> extends KeyframeSet {
private final float[] mValues;
private final Property<T, Float> mProperty;
public FloatKeyframeSet(Property<T, Float> property, float[] values) {
super(values.length);
mProperty = property;
mValues = values;
}
@Override
protected void interpolate(int index, float amount, Object target) {
float firstFloat = mValues[index - 1];
float secondFloat = mValues[index];
Log.i("Main12Activity", "index=" + index + " amount=" + amount + " target=" + target);
mProperty.set((T) target, firstFloat + (secondFloat - firstFloat) * amount);
}
}
private static class IntKeyframeSet<T> extends KeyframeSet {
private final int[] mValues;
private final Property<T, Integer> mProperty;
public IntKeyframeSet(Property<T, Integer> property, int[] values) {
super(values.length);
mProperty = property;
mValues = values;
}
@Override
protected void interpolate(int index, float amount, Object target) {
int firstFloat = mValues[index - 1];
int secondFloat = mValues[index];
mProperty.set((T) target, (int) (firstFloat + (secondFloat - firstFloat) * amount));
}
}
}
里面依赖了一个工具类,这里因为依赖比较少,我还是把它考出来了
import java.util.Random;
public class MathUtils {
private static final Random sRandom = new Random();
private static final float DEG_TO_RAD = 3.1415926f / 180.0f;
private static final float RAD_TO_DEG = 180.0f / 3.1415926f;
private MathUtils() {
}
public static float abs(float v) {
return v > 0 ? v : -v;
}
public static int constrain(int amount, int low, int high) {
return amount < low ? low : (amount > high ? high : amount);
}
public static long constrain(long amount, long low, long high) {
return amount < low ? low : (amount > high ? high : amount);
}
public static float constrain(float amount, float low, float high) {
return amount < low ? low : (amount > high ? high : amount);
}
public static float log(float a) {
return (float) Math.log(a);
}
public static float exp(float a) {
return (float) Math.exp(a);
}
public static float pow(float a, float b) {
return (float) Math.pow(a, b);
}
public static float max(float a, float b) {
return a > b ? a : b;
}
public static float max(int a, int b) {
return a > b ? a : b;
}
public static float max(float a, float b, float c) {
return a > b ? (a > c ? a : c) : (b > c ? b : c);
}
public static float max(int a, int b, int c) {
return a > b ? (a > c ? a : c) : (b > c ? b : c);
}
public static float min(float a, float b) {
return a < b ? a : b;
}
public static float min(int a, int b) {
return a < b ? a : b;
}
public static float min(float a, float b, float c) {
return a < b ? (a < c ? a : c) : (b < c ? b : c);
}
public static float min(int a, int b, int c) {
return a < b ? (a < c ? a : c) : (b < c ? b : c);
}
public static float dist(float x1, float y1, float x2, float y2) {
final float x = (x2 - x1);
final float y = (y2 - y1);
return (float) Math.hypot(x, y);
}
public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) {
final float x = (x2 - x1);
final float y = (y2 - y1);
final float z = (z2 - z1);
return (float) Math.sqrt(x * x + y * y + z * z);
}
public static float mag(float a, float b) {
return (float) Math.hypot(a, b);
}
public static float mag(float a, float b, float c) {
return (float) Math.sqrt(a * a + b * b + c * c);
}
public static float sq(float v) {
return v * v;
}
public static float dot(float v1x, float v1y, float v2x, float v2y) {
return v1x * v2x + v1y * v2y;
}
public static float cross(float v1x, float v1y, float v2x, float v2y) {
return v1x * v2y - v1y * v2x;
}
public static float radians(float degrees) {
return degrees * DEG_TO_RAD;
}
public static float degrees(float radians) {
return radians * RAD_TO_DEG;
}
public static float acos(float value) {
return (float) Math.acos(value);
}
public static float asin(float value) {
return (float) Math.asin(value);
}
public static float atan(float value) {
return (float) Math.atan(value);
}
public static float atan2(float a, float b) {
return (float) Math.atan2(a, b);
}
public static float tan(float angle) {
return (float) Math.tan(angle);
}
public static float lerp(float start, float stop, float amount) {
return start + (stop - start) * amount;
}
/**
* Returns an interpolated angle in degrees between a set of start and end
* angles.
* <p>
* Unlike {@link #lerp(float, float, float)}, the direction and distance of
* travel is determined by the shortest angle between the start and end
* angles. For example, if the starting angle is 0 and the ending angle is
* 350, then the interpolated angle will be in the range [0,-10] rather
* than [0,350].
*
* @param start the starting angle in degrees
* @param end the ending angle in degrees
* @param amount the position between start and end in the range [0,1]
* where 0 is the starting angle and 1 is the ending angle
* @return the interpolated angle in degrees
*/
public static float lerpDeg(float start, float end, float amount) {
final float minAngle = (((end - start) + 180) % 360) - 180;
return minAngle * amount + start;
}
public static float norm(float start, float stop, float value) {
return (value - start) / (stop - start);
}
public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) {
return maxStart + (maxStart - maxStop) * ((value - minStart) / (minStop - minStart));
}
public static int random(int howbig) {
return (int) (sRandom.nextFloat() * howbig);
}
public static int random(int howsmall, int howbig) {
if (howsmall >= howbig) return howsmall;
return (int) (sRandom.nextFloat() * (howbig - howsmall) + howsmall);
}
public static float random(float howbig) {
return sRandom.nextFloat() * howbig;
}
public static float random(float howsmall, float howbig) {
if (howsmall >= howbig) return howsmall;
return sRandom.nextFloat() * (howbig - howsmall) + howsmall;
}
public static void randomSeed(long seed) {
sRandom.setSeed(seed);
}
/**
* Returns the sum of the two parameters, or throws an exception if the resulting sum would
* cause an overflow or underflow.
* @throws IllegalArgumentException when overflow or underflow would occur.
*/
public static int addOrThrow(int a, int b) throws IllegalArgumentException {
if (b == 0) {
return a;
}
if (b > 0 && a <= (Integer.MAX_VALUE - b)) {
return a + b;
}
if (b < 0 && a >= (Integer.MIN_VALUE - b)) {
return a + b;
}
throw new IllegalArgumentException("Addition overflow: " + a + " + " + b);
}
}
注意,由于TouchAnimator使用到了一些新版本的Api,有可能部分属性(如TranslationZ)不能在老版本兼容,因此在生产环境中必须修改该类,提高兼容性
下面是使用demo
public class Main12Activity extends AppCompatActivity {
private static final String TAG = "Main12Activity";
ImageButton btn;
TouchAnimator slodDownAnim;
private float mLastY;
private float position;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main12);
btn = (ImageButton) findViewById(R.id.btn);
btn.ALPHA.set(btn, 0f);
TouchAnimator.Builder builder = new TouchAnimator.Builder();
builder.addFloat(btn, "translationY", 0, 1000);
builder.addFloat(btn, "rotation", 0, 360);
builder.addFloat(btn, "alpha", 0, 1);
builder.setListener(new TouchAnimator.Listener() {
@Override
public void onAnimationAtStart() {
Log.i(TAG, "onAnimationAtStart");
}
@Override
public void onAnimationAtEnd() {
Log.i(TAG, "onAnimationAtEnd");
}
@Override
public void onAnimationStarted() {
Log.i(TAG, "onAnimationStarted");
}
});
slodDownAnim = builder.build();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float y = event.getY();
float v = y - mLastY;
position += v;
if (position > 1000) {
position = 1000;
} else if (position < 0) {
position = 0;
}
slodDownAnim.setPosition(position / 1000);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
这里的布局只是一个ImageButton
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main12"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"/>
</RelativeLayout>
附一张效果图