1. 程式人生 > >android進階4step1:Android動畫處理與自定義View——SurfaceView

android進階4step1:Android動畫處理與自定義View——SurfaceView

SurfaceView簡介

  • 1、SurfaceView與View的區別
  • 2、SurfaceView的具體使用場景
  • 3、如何使用SurfaceView

一、SurfaceView與View的區別

  • 1、不使用onDraw
  • 2、非UI執行緒繪製
  • 3、獨立的Surface

二、SurfaceView的具體使用場景

  • 1、視訊播放
  • 2、一些炫酷的動畫效果 (一直在動的動畫)
  • 3、小遊戲

三、如何使用SurfaceView

  • 1、獲取SurfaceHolder物件
  • 2、監聽Surface的建立
  • 3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
  • 4、通過SurfaceHolder獲取Canvas進行繪製

固定模板:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/12/10 21:09<p>
 * <p>更改時間:2018/12/10 21:09<p>
 * <p>版本號:1<p>
 */
public class SurfaceViewTemplate extends SurfaceView implements Runnable {
    private Thread mThread;
    //多執行緒的同步
    private volatile boolean isRunning;

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        super(context, attrs);

        /**
         * 如何使用SurfaceView
         1、獲取SurfaceHolder物件
         2、監聽Surface的建立
         3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
         4、通過SurfaceHolder獲取Canvas進行繪製
         */
        //1.獲取SurfaceHolder物件
        SurfaceHolder holder = getHolder();
        //2.監聽Surface建立
        holder.addCallback(new SurfaceHolder.Callback() {

            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //監聽Surface建立完畢
                mThread = new Thread(SurfaceViewTemplate.this);
                //執行run方法
                mThread.start();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                isRunning = false;
            }
        });
    }

    @Override
    public void run() {
        while (isRunning) {
            drawSelf();
        }
    }

    /**
     * 3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
     * 4、通過SurfaceHolder獲取Canvas進行繪製
     */
    private void drawSelf() {
        Canvas canvas = null;
        try {
            canvas = getHolder().lockCanvas();
            if (canvas != null) {
                //draw
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (canvas != null) {
                //釋放canvas
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }
}

surface例項:

在子執行緒開啟while迴圈 保證子執行緒一直執行

(一直在動態繪製圓(動態改變半徑)( 1.先繪製白色背景(覆蓋之前繪製的圓) 2.再繪製圓)) 重複1、2步驟

完整程式碼:

public class SurfaceViewTemplate extends SurfaceView implements Runnable {
    private Thread mThread;
    //多執行緒的同步
    private volatile boolean isRunning;
    //畫筆
    private Paint mPaint;

    //定義圓的屬性
    private int mMinRadius;
    private int mMaxRadius;
    private int mRadius;
    private int mFlag;

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        super(context, attrs);

        /**
         * 三、如何使用SurfaceView
         1、獲取SurfaceHolder物件
         2、監聽Surface的建立
         3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
         4、通過SurfaceHolder獲取Canvas進行繪製
         */
        //1.獲取SurfaceHolder物件
        SurfaceHolder holder = getHolder();
        //2.監聽Surface建立
        holder.addCallback(new SurfaceHolder.Callback() {


            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //讓執行緒開啟
                isRunning = true;
                //監聽Surface建立完畢
                mThread = new Thread(SurfaceViewTemplate.this);
                //執行run方法
                mThread.start();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                isRunning = false;
            }
        });

        //SurfaceView設定屬性
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
        //初始化畫筆
        initPaint();

    }

    /**
     * 初始化畫筆
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setDither(true);
        //抗鋸齒
        mPaint.setAntiAlias(true);
        //空心圓
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.GREEN);
    }

    /**
     * 控制元件大小發生改變時呼叫 系統呼叫
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //-20 隨意寫的
        mMaxRadius = Math.min(w, h) / 2 - 20;
        mRadius = mMinRadius = 30;
    }

    @Override
    public void run() {
        while (isRunning) {
            //不斷的繪製
            drawSelf();
        }
    }

    /**
     * 3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
     * 4、通過SurfaceHolder獲取Canvas進行繪製
     */
    private void drawSelf() {
        Canvas canvas = null;
        try {
            canvas = getHolder().lockCanvas();
            if (canvas != null) {
                drawCircle(canvas);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (canvas != null) {
                //釋放canvas
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    /**
     * 畫圓的方法
     * 因為放到while裡要不斷迴圈繪製檢視
     * 1.第一次先畫個白色背景
     * 2.背景之上畫個圓
     * 迴圈1.2 動態改變 圓的半徑
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        //繪製背景(覆蓋之前的圓)
        canvas.drawColor(Color.WHITE);
        //繪製圓
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, mPaint);
        //動態改變 圓的半徑
        if (mRadius >= mMaxRadius) {
            mFlag = -1;
        } else if (mRadius <= mMinRadius) {
            mFlag = 1;
        }
        //達到最大值遞減 2個畫素 達到最小值遞減2個畫素
        mRadius += mFlag * 2;
    }
}

實現飛享的小鳥的案例  

先了解一下canvas畫矩形的方法:

public voiddrawRect(float left, float top, float right, float bottom,Paint paint)

           說明:繪製一個矩型。需要注明的是繪製矩形的引數和Java中的方法不一樣。

              該方法的引數圖解說明如下:

        各位看官請注意:圖中X、Y軸方向標記錯誤。 自己也懶得重新修正了。

           

           那麼,矩形的高 height = bottom  - right

                      矩形的寬 width  = right – left

       PS :假如drawRect的引數有誤,比如right < left ,Android是不會給我們檢查的,也不會提示相應的錯誤資訊,

           但它會繪畫出一個高或寬很小的矩形,可能不是你希望的。

佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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">

    <!--讓自定義的surfaceview 佔滿 並使用NoActionbar主題 xml中設定-->
    <com.demo.surfaceviewdemo.game.FlappyBird
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
FlappyBird.java   主要邏輯 迴圈繪製動畫 和動態改變鳥的高度 和 地板的x方向的值
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.RectF;
import android.support.annotation.DrawableRes;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import com.demo.surfaceviewdemo.R;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/12/10 21:09<p>
 * <p>更改時間:2018/12/10 21:09<p>
 * <p>版本號:1<p>
 */
public class FlappyBird extends SurfaceView implements Runnable {
    private Thread mThread;
    //多執行緒的同步
    private volatile boolean isRunning;

    //繪製的矩形區域
    private RectF mDestRect;
    //res
    private Bitmap mBg;
    private Bitmap mBirdbm;
    private Bitmap mFloorbm;

    //定義Floor物件
    private Floor mFloor;
    private Bird mBrid;

    //定義 底部x的移動速度
    private int mSpeed;

    //點選螢幕鳥上升的距離
    private static final int TOUCH_UP_SIZE = -16;
    private int mBirdUpDis;
    //鳥自由下落的大小
    private static final int SIZE_AUTO_DOWN = 2;
    private int mAutoDownDis;

    //中間值
    private int mTempBirdDis;


    public FlappyBird(Context context, AttributeSet attrs) {
        super(context, attrs);

        /**
         * 三、如何使用SurfaceView
         1、獲取SurfaceHolder物件
         2、監聽Surface的建立
         3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
         4、通過SurfaceHolder獲取Canvas進行繪製
         */
        //1.獲取SurfaceHolder物件
        SurfaceHolder holder = getHolder();
        //2.監聽Surface建立
        holder.addCallback(new SurfaceHolder.Callback() {


            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                //讓執行緒開啟
                isRunning = true;
                //監聽Surface建立完畢
                mThread = new Thread(FlappyBird.this);
                //執行run方法
                mThread.start();
                //當用戶點選home的時候,會重新繪製 定位鳥的位置
                //讓鳥重寫在螢幕中央
                mBrid.reset();
                mTempBirdDis = 0;
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                isRunning = false;
            }

        });

        //SurfaceView設定屬性
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
        //初始化資源
        initRes();
        //設定底部x的移動速度
        mSpeed = Util.dp2px(getContext(), 2);
        //每次點選上升的距離
        mBirdUpDis = Util.dp2px(getContext(), TOUCH_UP_SIZE);
        //每次自由下落的大小
        mAutoDownDis = Util.dp2px(getContext(), SIZE_AUTO_DOWN);

    }

    /**
     * 初始化資源
     */
    private void initRes() {
        mBg = loadBitmapByResId(R.drawable.bg1);
        mBirdbm = loadBitmapByResId(R.drawable.b1);
        mFloorbm = loadBitmapByResId(R.drawable.floor_bg);

    }

    //封裝初始化資源
    //@DrawableRes 註解 來增強程式碼的健壯性 必須要傳入一個 DrawableRes值
    private Bitmap loadBitmapByResId(@DrawableRes int resId) {
        return BitmapFactory.decodeResource(getResources(), resId);
    }

    /**
     * 控制元件大小發生改變時呼叫 系統呼叫
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //矩形區域
        mDestRect = new RectF(0, 0, w, h);
        mFloor = new Floor(getContext(), w, h, mFloorbm);
        mBrid = new Bird(getContext(), w, h, mBirdbm);
    }

    @Override
    public void run() {
        while (isRunning) {
            long start = System.currentTimeMillis();
            drawSelf();
            long end = System.currentTimeMillis();
            //為了避免浪費資源
            //限制每隔50ms繪製一次 自己定義
            if (end - start < 50) {
                try {
                    Thread.sleep(50 - (end - start));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 3、開啟非同步執行緒進行while迴圈(子執行緒可以不停地繪製)
     * 4、通過SurfaceHolder獲取Canvas進行繪製
     */
    private void drawSelf() {
        Canvas canvas = null;
        try {
            canvas = getHolder().lockCanvas();
            if (canvas != null) {
                //進行繪製工作
                logic();
                drawBg(canvas);
                drawBird(canvas);
                drawFloor(canvas);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (canvas != null) {
                //釋放canvas
                getHolder().unlockCanvasAndPost(canvas);
            }
        }
    }

    /***
     *執行一定的邏輯
     *
     */
    private void logic() {
        //動態改變Floor地板 x的位置讓它有動起來的效果
        mFloor.setX(mFloor.getX() - mSpeed);

        //動態改變鳥的下降的速度y越大 越靠近底部
        mTempBirdDis += mAutoDownDis;
        mBrid.set(mBrid.getY() + mTempBirdDis);
    }

    //點選螢幕的世界
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            //賦值給中間變數 讓 y減小 即上升
            mTempBirdDis = mBirdUpDis;
        }
        return true;
    }

    //繪製背景
    private void drawBg(Canvas canvas) {
        //引數1 bitmap,matrix,繪製矩形區域
        canvas.drawBitmap(mBg, null, mDestRect, null);
    }

    /**
     * 對於下面有具體邏輯的繪製
     * 自定義類實現具體邏輯方便些
     *
     * @param canvas
     */
    //繪製地板
    private void drawFloor(Canvas canvas) {
        mFloor.draw(canvas);
    }

    //繪製小鳥
    private void drawBird(Canvas canvas) {
        mBrid.draw(canvas);
    }


}

工具類 Util.java 將dp轉換成px

public class Util {
    //將dp轉成 px
    public static int dp2px(Context context, int dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal,
                context.getResources().getDisplayMetrics());
    }
}

Bird和Floor的基類  抽取的公共方法

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/12/10 23:09<p>
 * <p>更改時間:2018/12/10 23:09<p>
 * <p>版本號:1<p>
 */

/**
 * 地板和鳥的公共類
 */
public abstract class DrawablePart {
    protected Context mContext;
    protected int mGameWidth;
    protected int mGameHeight;
    protected Bitmap mBitmap;

    public DrawablePart(Context context, int gameW, int gameH, Bitmap bitmap) {
        mContext = context;
        mGameWidth = gameW;
        mGameHeight = gameH;
        mBitmap = bitmap;
    }

    public abstract void draw(Canvas canvas);

}

Bird.java   實現鳥的繪製

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RectF;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/12/10 23:12<p>
 * <p>更改時間:2018/12/10 23:12<p>
 * <p>版本號:1<p>
 */
public class Bird extends DrawablePart {
    private int x;
    private int y;

    private static final float RADIO_Y_POS = 1 / 2F;

    // 30dp
    private static final int WIDTH_BIRD = 30;

    private int mWidth;
    private int mHeight;

    //繪製的矩形範圍
    private RectF mRect = new RectF();

    public Bird(Context context, int gameW, int gameH, Bitmap bitmap) {
        super(context, gameW, gameH, bitmap);
        //鳥所在的y的位置
        y = (int) (gameH * RADIO_Y_POS);
        //設定鳥的寬度為30dp
        mWidth = Util.dp2px(context, WIDTH_BIRD);
        //在和寬度等比例下 鳥的高度
        mHeight = (int) (mWidth * 1.0f / bitmap.getWidth() * bitmap.getHeight());
        //繪製鳥的初始左邊x位置 螢幕的一半減去鳥本身的寬度的一半
        x = gameW / 2 - mWidth / 2;
    }

    @Override
    public void draw(Canvas canvas) {
        mRect.set(x, y, x + mWidth, y + mHeight);
        canvas.drawBitmap(mBitmap, null, mRect, null);
    }

    /**
     * 重置bird位置
     * 改變高度
     */
    public void reset() {
        y = (int) (mHeight * RADIO_Y_POS);
    }

    /**
     * 暴露方法點選時動態改變y的值
     *
     * @return
     */
    public int getY() {
        return y;
    }

    public void set(int y) {
        this.y = y;
    }
}

Floor.java 實現地板的繪製

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Shader;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/12/10 23:08<p>
 * <p>更改時間:2018/12/10 23:08<p>
 * <p>版本號:1<p>
 */
public class Floor extends DrawablePart {

    //設定動畫的位置
    private int x;
    private int y;
    private static final float RADIO_Y_POS = 4 / 5F;
    private Paint mPaint;
    private BitmapShader mBitmapShader;


    public Floor(Context context, int gameW, int gameH, Bitmap bitmap) {
        super(context, gameW, gameH, bitmap);
        //讓高度位於檢視的4/5左右
        y = (int) (gameH * RADIO_Y_POS);
        //初始化畫筆
        mPaint = new Paint();
        //設定抗鋸齒
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        //引數1 bitmap 引數2:橫向的模式(設定為重複) 引數3:縱向的模式(設定為拉伸)
        mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.save();
        //將畫步canvas移動到指定的位置
        canvas.translate(x, y);
        mPaint.setShader(mBitmapShader);
        canvas.drawRect(x, 0, mGameWidth - x, mGameHeight - y, mPaint);
        canvas.restore();
        //繪製完成之後 回收shader
        mPaint.setShader(null);
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
        //如果x的大於螢幕寬度 則取餘
        if (-x > mGameWidth) {
            this.x = x % mGameWidth;
        }
    }
}