Android 基础

Android 基础之动画和手势

2019-02-12  本文已影响3人  Kevin_小飞象

Android中动画分为3种:

  1. Tween Animation(补间动画):通过对场景的对象不断做图像变换(平移、缩放、旋转)产生动画效果,即是一种渐变动画。
  2. Frame Animation(逐帧动画):顺序播放事先做好的图像,是一种画面转换动画。
  3. Property Animation(属性动画):通过动态地改变对象的属性从而达到动画效果,属性动画为 API 11 新特性。

Tween Animation

根据不同的动画效果,补间动画分为 4 种动画:

具体使用

方法1.通过xml实现
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:fromDegrees="0" // 动画开始时 视图的旋转角度(正数 = 顺时针,负数 = 逆时针)
    android:toDegrees="270" // 动画结束时 视图的旋转角度(正数 = 顺时针,负数 = 逆时针)
    android:pivotX="50%" // 旋转轴点的x坐标
    android:pivotY="0" // 旋转轴点的y坐标
    /> 
public class MainActivity extends AppCompatActivity {
    private Button mButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = findViewById(R.id.button);

        Animation rotateAnimation = AnimationUtils.loadAnimation(this, R.anim.rotate);

        mButton.startAnimation(rotateAnimation);

    }
}
方法2:代码设置
        Button mButton = (Button) findViewById(R.id.Button);
          Animation rotateAnimation = new RotateAnimation(0,270,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
        rotateAnimation.setDuration(3000);
        mButton.startAnimation(rotateAnimation);

参考:Android 动画:手把手教你使用 补间动画

Frame Animation

Frame Animation 是顺序播放事先做好的图像,跟电影类似。不同于animation package,Android SDK 提供了另外一个类 AnimationDrawable 来定义使用 Frame Animation。

具体使用

<?xml version="1.0" encoding="utf-8"?>
 
<animation-list
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:oneshot="true"
  >
       <item android:drawable="@drawable/p1" android:duration="1000"></item>
       <item android:drawable="@drawable/p2" android:duration="1000"></item>
       <item android:drawable="@drawable/p3" android:duration="1000"></item>
       <item android:drawable="@drawable/p4" android:duration="1000"></item>
       <item android:drawable="@drawable/p5" android:duration="1000"></item>
       <item android:drawable="@drawable/p6" android:duration="1000"></item>
</animation-list>

step2:在Java中实现
FrameActivity.java

public class FrameActivity extends AppCompatActivity {
    private Button btn_startFrame,btn_stopFrame;
    private ImageView iv;
    private AnimationDrawable animationDrawable;

        iv = (ImageView) findViewById(R.id.iv);
        btn_startFrame = (Button) findViewById(R.id.btn_startFrame);
        btn_stopFrame = (Button) findViewById(R.id.btn_stopFrame);


        <-- 开始动画 -->
        btn_startFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                iv.setImageResource(R.drawable.frame);
                // 1. 设置动画
                animationDrawable = (AnimationDrawable) iv.getDrawable();
                // 2. 获取动画对象
                animationDrawable.start();
                // 3. 启动动画
            }
        });
        //停止动画
        btn_stopFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
                iv.setImageResource(R.drawable.frame);
                // 1. 设置动画
                animationDrawable = (AnimationDrawable) iv.getDrawable();
                // 2. 获取动画对象
                animationDrawable.stop();
                // 3. 暂停动画
            }
        });

    }
}
<-- 直接从drawable文件夹获取动画资源(图片) -->
        animationDrawable = new AnimationDrawable();
        for (int i = 0; i <= 25; i++) {
            int id = getResources().getIdentifier("a" + i, "drawable", getPackageName());
            Drawable drawable = getResources().getDrawable(id);
            animationDrawable.addFrame(drawable, 100);
        }

        <-- 开始动画 -->
        btn_startFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                animationDrawable.setOneShot(true);
                iv.setImageDrawable(animationDrawable);
                // 获取资源对象
                animationDrawable.stop();
                 // 特别注意:在动画start()之前要先stop(),不然在第一次动画之后会停在最后一帧,这样动画就只会触发一次
                animationDrawable.start();
                // 启动动画
               
            }
        });

         <-- 停止动画 -->
        btn_stopFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                animationDrawable.setOneShot(true);
                iv.setImageDrawable(animationDrawable);
                animationDrawable.stop();
            }
        });

参考:Android 逐帧动画:关于 逐帧动画 的使用都在这里了!

Property Animation

原理

属性动画要求动画作用的对象提供该属性的 get 和 set 方法,属性动画根据你传递的该熟悉的初始值和最终值,以动画的效果多次去调用 set 方法,每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,你对 object 的属性 xxx 做动画,如果想让动画生效,要同时满足两个条件:

1. object 必须要提供 setXxx 方法,如果动画的时候没有传递初始值,那么还要提供 getXxx 方法,因为系统要去拿 xxx 属性的初始值(如果这条不满足,程序直接 Crash)

2. object 的 setXxx 对属性 xxx 所做的改变必须能够通过某种方法反映出来,比如会带来 UI 的改变啥的(如果这条不满足,动画无效果但不会 Crash)

以上条件缺一不可。

具体使用

1.ValueAnimator 类
ValueAnimator类中有3个重要方法:

  1. ValueAnimator.ofInt(int values)作用:将初始值以整型数值的形式过渡到结束值
  2. ValueAnimator.ofFloat(float values)作用:将初始值以浮点型数值的形式过渡到结束值
  3. ValueAnimator.ofObject(int values) 作用:将初始值以对象的形式过渡到结束值

实例:按钮的宽度从 150px 放大到 500px。

public class MainActivity extends AppCompatActivity {
    private Button mButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = findViewById(R.id.button);

        ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);

        valueAnimator.setDuration(2000);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {

                int currentValue = (Integer) animator.getAnimatedValue();

                mButton.getLayoutParams().width = currentValue;

                mButton.requestLayout();

            }
        });
        valueAnimator.start();
    }
}

效果图:


效果图

2.ObjectAnimator类

旋转

public class MainActivity extends AppCompatActivity {
    private Button mButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = findViewById(R.id.button);

        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
        animator.setDuration(5000);
        animator.start();
    }
}

效果图


旋转

参考:Android 属性动画:这是一篇很详细的 属性动画 总结&攻略

手势检测(GestureDetector)

在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。
实例:轨迹球
FailingBall.java

package com.scarf.demo007.widget;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

import com.scarf.demo007.R;

/**
 * Created on 2019/2/12 15:09
 * 轨迹球
 * @author Scarf Gong
 */
public class FailingBall extends View {
    private int mWidth;             // 宽度
    private int mHeight;            // 高度

    private float mStartX = 0;        // 小方块开始位置X
    private float mStartY = 0;        // 小方块开始位置Y
    private float mEdgeLength = 200;  // 边长
    private RectF mRect = new RectF(mStartX, mStartY, mStartX + mEdgeLength, mStartY + mEdgeLength);

    private float mFixedX = 0;  // 修正距离X
    private float mFixedY = 0;  // 修正距离Y

    private Paint mPaint;

    private GestureDetector mGestureDetector;
    private boolean mCanFail = false;   // 是否可以拖动

    private float mSpeedX = 0;
    private float mSpeedY = 0;

    private Boolean mXFixed = false;
    private Boolean mYFixed = false;

    private Bitmap mBitmap;

    public FailingBall(Context context) {
        this(context,null);
    }

    public FailingBall(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public FailingBall(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setAntiAlias(true);

        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ball);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mStartX = (w - mEdgeLength) / 2;
        mStartY = (h - mEdgeLength) / 2;
        refreshRectByCurrentPoint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawOval(mRect, mPaint);
        canvas.drawBitmap(mBitmap, new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()),
                mRect, mPaint);
    }

    // 每 100 ms 更新一次
    private Handler mHandler = new Handler();
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            mStartX = mStartX + mSpeedX / 30;
            mStartY = mStartY + mSpeedY / 30;
            //mSpeedX = mSpeedX > 0 ? mSpeedX - 10 : mSpeedX + 10;
            //mSpeedY = mSpeedY > 0 ? mSpeedY - 10 : mSpeedY + 10;
            mSpeedX *= 0.97;
            mSpeedY *= 0.97;
            if (Math.abs(mSpeedX) < 10) {
                mSpeedX = 0;
            }
            if (Math.abs(mSpeedY) < 10) {
                mSpeedY = 0;
            }
            if (refreshRectByCurrentPoint()) {
                // 转向
                if (mXFixed) {
                    mSpeedX = -mSpeedX;
                }
                if (mYFixed) {
                    mSpeedY = -mSpeedY;
                }
            }
            invalidate();
            if (mSpeedX == 0 && mSpeedY == 0) {
                mHandler.removeCallbacks(this);
                return;
            }
            mHandler.postDelayed(this, 33);
        }
    };

    private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new
            GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
                        velocityY) {
                    Log.e("Failing", velocityX + " : " + velocityY);
                    if (!mCanFail) return false;
                    mSpeedX = velocityX;
                    mSpeedY = velocityY;
                    mHandler.removeCallbacks(mRunnable);
                    mHandler.postDelayed(mRunnable, 0);
                    return super.onFling(e1, e2, velocityX, velocityY);
                }
            };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                if (contains(event.getX(), event.getY())) {
                    mCanFail = true;
                    mFixedX = event.getX() - mStartX;
                    mFixedY = event.getY() - mStartY;
                    mSpeedX = 0;
                    mSpeedY = 0;
                } else {
                    mCanFail = false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mCanFail) {
                    break;
                }
                mStartX = event.getX() - mFixedX;
                mStartY = event.getY() - mFixedY;
                if (refreshRectByCurrentPoint()) {
                    mFixedX = event.getX() - mStartX;
                    mFixedY = event.getY() - mStartY;
                }
                invalidate();
                break;
        }
        return true;
    }

    private Boolean contains(float x, float y) {
        float radius = mEdgeLength / 2;
        float centerX = mRect.left + radius;
        float centerY = mRect.top + radius;
        return Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) <= radius;
    }

    /**
     * 刷新方块位置
     *
     * @return true 表示修正过位置, false 表示没有修正过位置
     */
    private Boolean refreshRectByCurrentPoint() {
        Boolean fixed = false;
        mXFixed = false;
        mYFixed = false;
        // 修正坐标
        if (mStartX < 0) {
            mStartX = 0;
            fixed = true;
            mXFixed = true;
        }
        if (mStartY < 0) {
            mStartY = 0;
            fixed = true;
            mYFixed = true;
        }
        if (mStartX + mEdgeLength > mWidth) {
            mStartX = mWidth - mEdgeLength;
            fixed = true;
            mXFixed = true;
        }
        if (mStartY + mEdgeLength > mHeight) {
            mStartY = mHeight - mEdgeLength;
            fixed = true;
            mYFixed = true;
        }
        mRect.left = mStartX;
        mRect.top = mStartY;
        mRect.right = mStartX + mEdgeLength;
        mRect.bottom = mStartY + mEdgeLength;
        return fixed;
    }
}

布局文件:
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

    <com.scarf.demo007.widget.FailingBall
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

效果图


轨迹球

缩放手势检测(ScaleGestureDecetor)

缩放手势对于大部分 Android 工程师来说,需要用到的机会比较少,它最常见于以下的一些应用场景中,例如:图片浏览,图片编辑(贴图效果)、网页缩放、地图、文本阅读(通过缩放手势调整文字大小)等。

代码:

/**
 * Created on 2019/2/12 15:30
 *
 * @author Scarf Gong
 */
public class GestureDemoView extends View {
    GestureDetector mGestureDetector;
    ScaleGestureDetector mScaleGestureDetector;

    // 画布当前的 Matrix, 用于获取当前画布的一些状态信息,例如缩放大小,平移距离等
    private Matrix mCanvasMatrix = new Matrix();

    // 将用户触摸的坐标转换为画布上坐标所需的 Matrix, 以便找到正确的缩放中心位置
    private Matrix mInvertMatrix = new Matrix();

    // 所有用户触发的缩放、平移等操作都通过下面的 Matrix 直接作用于画布上,
    // 将系统计算的一些初始缩放平移信息与用户操作的信息进行隔离,让操作更加直观
    private Matrix mUserMatrix = new Matrix();

    private Bitmap mBitmap;

    // 基础的缩放和平移信息,该信息与用户的手势操作无关
    private float mBaseScale;
    private float mBaseTranslateX;
    private float mBaseTranslateY;

    private Paint mPaint;

    public GestureDemoView(Context context) {
        super(context);
    }

    public GestureDemoView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        initGesture(context);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (mBitmap.getWidth() * 1.0f / mBitmap.getHeight() > w * 1.0f / h) {
            mBaseScale = w * 1.0f / mBitmap.getWidth();
            mBaseTranslateX = 0;
            mBaseTranslateY = (h - mBitmap.getHeight() * mBaseScale) / 2;
        } else {
            mBaseScale = h * 1.0f / mBitmap.getHeight() * 1.0f;
            mBaseTranslateX = (w - mBitmap.getWidth() * mBaseScale) / 2;
            mBaseTranslateY = 0;
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        canvas.translate(mBaseTranslateX, mBaseTranslateY);
        canvas.scale(mBaseScale, mBaseScale);

        canvas.save();
        canvas.concat(mUserMatrix);

        mCanvasMatrix = canvas.getMatrix();
        mCanvasMatrix.invert(mInvertMatrix);

        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        canvas.restore();
    }


    //--- 手势处理 ----------------------------------------------------------------------------------

    private void initGesture(Context context) {
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                float scale = getMatrixValue(MSCALE_X, mCanvasMatrix);
                mUserMatrix.preTranslate(-distanceX / scale, -distanceY / scale);
                //fixTranslate();   // 在用户滚动时不进行修正,保证用户滚动时也有响应, 在用户抬起手指后进行修正
                invalidate();
                return true;
            }

            @Override
            public boolean onDoubleTap(MotionEvent e) {
                if (!mUserMatrix.isIdentity()) {
                    mUserMatrix.reset();
                } else {
                    float[] points = mapPoint(e.getX(), e.getY(), mInvertMatrix);
                    mUserMatrix.postScale(MAX_SCALE, MAX_SCALE, points[0], points[1]);
                }
                fixTranslate();
                invalidate();
                return true;
            }
        });

        mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                float scaleFactor = detector.getScaleFactor();
                float fx = detector.getFocusX();
                float fy = detector.getFocusY();
                float[] points = mapPoint(fx, fy, mInvertMatrix);
                scaleFactor = getRealScaleFactor(scaleFactor);
                mUserMatrix.preScale(scaleFactor, scaleFactor, points[0], points[1]);
                fixTranslate();
                invalidate();
                return true;
            }

        });
    }

    // 修正缩放
    private void fixTranslate() {
        // 对 Matrix 进行预计算,并根据计算结果进行修正
        Matrix viewMatrix = getMatrix();    // 获取当前控件的Matrix
        viewMatrix.preTranslate(mBaseTranslateX, mBaseTranslateY);
        viewMatrix.preScale(mBaseScale, mBaseScale);
        viewMatrix.preConcat(mUserMatrix);
        Matrix invert = new Matrix();
        viewMatrix.invert(invert);
        Rect rect = new Rect();
        getGlobalVisibleRect(rect);

        float userScale = getMatrixValue(MSCALE_X, mUserMatrix);
        float scale = getMatrixValue(MSCALE_X, viewMatrix);

        float[] center = mapPoint(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, viewMatrix);
        float distanceX = center[0] - getWidth() / 2.0f;
        float distanceY = center[1] - getHeight() / 2.0f;
        float[] wh = mapVectors(mBitmap.getWidth(), mBitmap.getHeight(), viewMatrix);

        if (userScale <= 1.0f) {
            mUserMatrix.preTranslate(-distanceX / scale, -distanceY / scale);
        } else {
            float[] lefttop = mapPoint(0, 0, viewMatrix);
            float[] rightbottom = mapPoint(mBitmap.getWidth(), mBitmap.getHeight(), viewMatrix);

            // 如果宽度小于总宽度,则水平居中
            if (wh[0] < getWidth()) {
                mUserMatrix.preTranslate(distanceX / scale, 0);
            } else {
                if (lefttop[0] > 0) {
                    mUserMatrix.preTranslate(-lefttop[0] / scale, 0);
                } else if (rightbottom[0] < getWidth()) {
                    mUserMatrix.preTranslate((getWidth() - rightbottom[0]) / scale, 0);
                }

            }
            // 如果高度小于总高度,则垂直居中
            if (wh[1] < getHeight()) {
                mUserMatrix.preTranslate(0, -distanceY / scale);
            } else {
                if (lefttop[1] > 0) {
                    mUserMatrix.preTranslate(0, -lefttop[1] / scale);
                } else if (rightbottom[1] < getHeight()) {
                    mUserMatrix.preTranslate(0, (getHeight() - rightbottom[1]) / scale);
                }
            }
        }
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        mScaleGestureDetector.onTouchEvent(event);
        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
            fixTranslate();
        }
        return true;
    }


    //--- Tools ------------------------------------------------------------------------------------

    //--- 将坐标转换为画布坐标 ---
    private float[] mapPoint(float x, float y, Matrix matrix) {
        float[] temp = new float[2];
        temp[0] = x;
        temp[1] = y;
        matrix.mapPoints(temp);
        return temp;
    }

    private float[] mapVectors(float x, float y, Matrix matrix) {
        float[] temp = new float[2];
        temp[0] = x;
        temp[1] = y;
        matrix.mapVectors(temp);
        return temp;
    }


    //--- 获取 Matrix 中的属性 ---
    private float[] matrixValues = new float[9];
    private static final int MSCALE_X = 0, MSKEW_X = 1, MTRANS_X = 2;
    private static final int MSKEW_Y = 3, MSCALE_Y = 4, MTRANS_Y = 5;
    private static final int MPERSP_0 = 6, MPERSP_1 = 7, MPERSP_2 = 8;

    @IntDef({MSCALE_X, MSKEW_X, MTRANS_X, MSKEW_Y, MSCALE_Y, MTRANS_Y, MPERSP_0, MPERSP_1, MPERSP_2})
    @Retention(RetentionPolicy.SOURCE)
    private @interface MatrixName {}

    private float getMatrixValue(@MatrixName int name, Matrix matrix) {
        matrix.getValues(matrixValues);
        return matrixValues[name];
    }

    //--- 限制缩放比例 ---
    private static final float MAX_SCALE = 4.0f;    //最大缩放比例
    private static final float MIN_SCALE = 0.5f;    // 最小缩放比例

    private float getRealScaleFactor(float currentScaleFactor) {
        float realScale = 1.0f;
        float userScale = getMatrixValue(MSCALE_X, mUserMatrix);    // 用户当前的缩放比例
        float theoryScale = userScale * currentScaleFactor;           // 理论缩放数值

        // 如果用户在执行放大操作并且理论缩放数据大于4.0
        if (currentScaleFactor > 1.0f && theoryScale > MAX_SCALE) {
            realScale = MAX_SCALE / userScale;
        } else if (currentScaleFactor < 1.0f && theoryScale < MIN_SCALE) {
            realScale = MIN_SCALE / userScale;
        } else {
            realScale = currentScaleFactor;
        }
        return realScale;
    }
}

上一篇 下一篇

猜你喜欢

热点阅读