標籤:android canvas動態繪圖
一直想實現一個動態繪圖的功能,就是那種給定幾張圖片之後一張張的順序畫出來。說不明白,先上。
這樣可以做很多東西,像百度地圖的曆史軌跡繪製,引導介面做類似動畫效果等。
之前我考慮用SurfaceView實現這個功能,想一想,要實現這種效果,需要開啟一個子線程用於控制繪製時間間隔,以達到這種漸漸繪製的效果。動手去做了,發現用SurfaceView很難實現,SurfaceView中的Canvas與View中的Canvas不同,一個不同之處是View中的Canvas是只有一張畫布,然後不停的在這張畫布上操作,你所有畫的圖都是在一張畫布上,但是SurfaceView不是的,SurfaceView中每次通過holder.lockCanvas()得到的都是一張新的畫布,通過holder.unlockCanvasAndpost(canvas)提交之後會覆蓋掉之前的畫布,也就是說後面繪製的東西會把前面繪製的東西給遮擋住,另一個不同之處在SurfaceView可以在子線程中繪圖,但是繪製之後並不馬上顯示出來,只有在holder.unlockCanvasAndpost(canvas)之後才會在顯示出來,這一點與View中的canvas是明顯不同的。基於以上兩個不同,最後我還是採用了自訂View,利用View中的Canvas進行繪製。
繪製步驟:
(1)繪製傳入的映像
(2)繪製第一張映像與第二張映像之間的線,這一步又分為兩個小步
a、從第一張圖中心店開始增加固定步長,一遍遍的繪製,直到接近第二張圖的中心點
b、a繪製完成之後,重新繪製一條直線,路徑跟a的路徑保持一致,將a繪製的直線擦除
(3)重複(1)、(2)步,注意最後一張圖片繪製完之後不用再繪線。
繪製過程中需要不停的控制時間間隔,也就意味需要不停的開啟新的子線程,這樣是很消耗記憶體的,極易造成記憶體溢出,所以這裡我採用了線程池來管理線程。
代碼:
建立一個項目AnimationView
建立一個類AnimationView繼承View
添加建構函式,在建構函式中開啟我們的線程池。
private void startExecutor() {executorThread = new Thread() {@Overridepublic void run() {// TODO Auto-generated method stubsuper.run();Looper.prepare();threadHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubsuper.handleMessage(msg);executorService.execute(getTask());try {semaphore.acquire();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}try {Thread.sleep(100);} catch (InterruptedException e1) {e1.printStackTrace();}}};handlerSemaphore.release();Looper.loop();}};executorThread.start();}
public AnimationView(Context context) {super(context);// TODO Auto-generated constructor stubstartExecutor();}public AnimationView(Context context, AttributeSet attrs) {super(context, attrs);// TODO Auto-generated constructor stubstartExecutor();}public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// TODO Auto-generated constructor stubstartExecutor();}
關於線程池這裡不多講,想瞭解的話可以看看我的另一篇文章Android 線程池、訊號量、Looper、緩衝初探
添加兩個方法用於新增工作和取出任務
private synchronized void addTask(Runnable r) {if (threadHandler == null)try {handlerSemaphore.acquire();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}task.add(r);threadHandler.obtainMessage().sendToTarget();}private synchronized Runnable getTask() {return task.removeFirst();}
添加一個方法用於向我們自訂的View添加圖片及位置
public void addImageData(int id, Rect rect) {ImageData data = new ImageData();data.setId(id);data.setRect(rect);list.add(data);}ImageData是自訂的一個類
package com.example.animationview;import android.graphics.Rect;public class ImageData {private int id;private Rect rect;public int getId() {return id;}public void setId(int id) {this.id = id;}public Rect getRect() {return rect;}public void setRect(Rect rect) {this.rect = rect;}}覆寫onDraw函數
@Overrideprotected void onDraw(Canvas canvas) {// TODO Auto-generated method stubsuper.onDraw(canvas);if (this.list != null && this.list.size() != 0) {// 畫圖for (int i = 0; i <= index; i++) {canvas.save();ImageData data = this.list.get(i);Bitmap bitmap = BitmapFactory.decodeResource(getResources(),data.getId());canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(),bitmap.getHeight()), data.getRect(), new Paint());canvas.restore();invalidate();bitmap = null;}if (isDrawBitmap && index <= this.list.size() - 2)// 判斷後面還有沒有圖,有圖則畫線{isDrawBitmap = false;isDrawStep = true;isSendStepHandler = true;stepStartX = 0;stepStartY = 0;}// 畫中間步驟if (isDrawStep) {ImageData data1 = this.list.get(index);ImageData data2 = this.list.get(index + 1);if (stepStartX == 0 && stepStartY == 0) {stepStartX = data1.getRect().centerX();stepStartY = data1.getRect().centerY();stepX = ((float) (data2.getRect().centerX() - data1.getRect().centerX())) / 16;stepY = ((float) (data2.getRect().centerY() - data1.getRect().centerY())) / 16;stepEndX = stepStartX + stepX;stepEndY = stepStartY + stepY;}canvas.save();Paint paint = new Paint();paint.setStrokeWidth(3);paint.setStyle(Style.STROKE);paint.setAntiAlias(true);paint.setColor(Color.RED);canvas.drawLine(stepStartX, stepStartY, stepEndX, stepEndY,paint);canvas.restore();invalidate();System.out.println("Math: "+ Math.abs((double) (stepEndX - data2.getRect().centerX())) + " "+ Math.abs((double) stepX));if (Math.abs((double) (stepEndX - data2.getRect().centerX())) <= Math.abs((double) stepX)&& Math.abs((double) (stepEndY - data2.getRect().centerY())) <= Math.abs((double) stepY))// 畫線{isStopDrawStep = true;if (isSendStepHandler) {isSendStepHandler = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(2).sendToTarget();semaphore.release();}});}} else {// 畫中間步驟if (isSendStepHandler) {isSendStepHandler = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(1).sendToTarget();semaphore.release();}});}}}// 畫線for (int i = 0; i <= lineIndex; i++) {ImageData data1 = this.list.get(i);ImageData data2 = this.list.get(i + 1);startX = data1.getRect().centerX();startY = data1.getRect().centerY();endX = data2.getRect().centerX();endY = data2.getRect().centerY();canvas.save();Paint paint = new Paint();paint.setStrokeWidth(3);paint.setStyle(Style.STROKE);paint.setAntiAlias(true);paint.setColor(Color.RED);canvas.drawLine(startX, startY, endX, endY, paint);canvas.restore();invalidate();if (isStopDrawStep && (recodeLineIndex != lineIndex)) {recodeLineIndex = lineIndex;isDrawStep = false;isStopDrawStep = false;}}if (isDrawLine && lineIndex <= this.list.size() - 2) {isDrawLine = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(0).sendToTarget();semaphore.release();}});}}}
這裡是關鍵代碼,裡面我做了簡單的注釋,另外可以看到我在裡面使用了大量的Boolean類型的變數用於控制繪製頻率,不得不這樣做,因為不做的話我們的View是不停的在重新整理的,不加以控制,會產生很多的空任務,用線程池管理可以還不容易看出來,如果把線程池換成普通的子線程,你會發現,你的程式極有可能不到5秒鐘就崩潰了,崩潰原因就是記憶體溢出。
AnimationView的完整代碼
package com.example.animationview;import java.util.LinkedList;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;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.Paint.Style;import android.graphics.Rect;import android.os.Handler;import android.os.Looper;import android.os.Message;import android.util.AttributeSet;import android.view.View;public class AnimationView extends View {private boolean isDrawBitmap = true, isDrawLine = false;private boolean isDrawStep = false;private boolean isStopDrawStep = false;private boolean isSendStepHandler = false;private int recodeLineIndex = -1;private List<ImageData> list = new LinkedList<ImageData>();private int index = 0, lineIndex = -1;// bitmap索引private float startX = 0, startY = 0, endX, endY;// 畫線起點,間距private float stepStartX, stepStartY, stepX, stepY, stepEndX, stepEndY;private ExecutorService executorService = Executors.newFixedThreadPool(3);private Semaphore handlerSemaphore = new Semaphore(0);private Semaphore semaphore = new Semaphore(3);private LinkedList<Runnable> task = new LinkedList<Runnable>();private Handler threadHandler;private Thread executorThread;private Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubsuper.handleMessage(msg);switch (msg.what) {case 0:isDrawBitmap = true;if (index < list.size() - 1)index++;break;case 1:isSendStepHandler = true;stepEndX += stepX;stepEndY += stepY;break;case 2:isDrawLine = true;if (lineIndex < list.size() - 2)lineIndex++;break;}}};public AnimationView(Context context) {super(context);// TODO Auto-generated constructor stubstartExecutor();}public AnimationView(Context context, AttributeSet attrs) {super(context, attrs);// TODO Auto-generated constructor stubstartExecutor();}public AnimationView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// TODO Auto-generated constructor stubstartExecutor();}private void startExecutor() {executorThread = new Thread() {@Overridepublic void run() {// TODO Auto-generated method stubsuper.run();Looper.prepare();threadHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubsuper.handleMessage(msg);executorService.execute(getTask());try {semaphore.acquire();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}try {Thread.sleep(100);} catch (InterruptedException e1) {e1.printStackTrace();}}};handlerSemaphore.release();Looper.loop();}};executorThread.start();}private synchronized void addTask(Runnable r) {if (threadHandler == null)try {handlerSemaphore.acquire();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}task.add(r);threadHandler.obtainMessage().sendToTarget();}private synchronized Runnable getTask() {return task.removeFirst();}public void addImageData(int id, Rect rect) {ImageData data = new ImageData();data.setId(id);data.setRect(rect);list.add(data);}@Overrideprotected void onDraw(Canvas canvas) {// TODO Auto-generated method stubsuper.onDraw(canvas);if (this.list != null && this.list.size() != 0) {// 畫圖for (int i = 0; i <= index; i++) {canvas.save();ImageData data = this.list.get(i);Bitmap bitmap = BitmapFactory.decodeResource(getResources(),data.getId());canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(),bitmap.getHeight()), data.getRect(), new Paint());canvas.restore();invalidate();bitmap = null;}if (isDrawBitmap && index <= this.list.size() - 2)// 判斷後面還有沒有圖,有圖則畫線{isDrawBitmap = false;isDrawStep = true;isSendStepHandler = true;stepStartX = 0;stepStartY = 0;}// 畫中間步驟if (isDrawStep) {ImageData data1 = this.list.get(index);ImageData data2 = this.list.get(index + 1);if (stepStartX == 0 && stepStartY == 0) {stepStartX = data1.getRect().centerX();stepStartY = data1.getRect().centerY();stepX = ((float) (data2.getRect().centerX() - data1.getRect().centerX())) / 16;stepY = ((float) (data2.getRect().centerY() - data1.getRect().centerY())) / 16;stepEndX = stepStartX + stepX;stepEndY = stepStartY + stepY;}canvas.save();Paint paint = new Paint();paint.setStrokeWidth(3);paint.setStyle(Style.STROKE);paint.setAntiAlias(true);paint.setColor(Color.RED);canvas.drawLine(stepStartX, stepStartY, stepEndX, stepEndY,paint);canvas.restore();invalidate();System.out.println("Math: "+ Math.abs((double) (stepEndX - data2.getRect().centerX())) + " "+ Math.abs((double) stepX));if (Math.abs((double) (stepEndX - data2.getRect().centerX())) <= Math.abs((double) stepX)&& Math.abs((double) (stepEndY - data2.getRect().centerY())) <= Math.abs((double) stepY))// 畫線{isStopDrawStep = true;if (isSendStepHandler) {isSendStepHandler = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(2).sendToTarget();semaphore.release();}});}} else {// 畫中間步驟if (isSendStepHandler) {isSendStepHandler = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(1).sendToTarget();semaphore.release();}});}}}// 畫線for (int i = 0; i <= lineIndex; i++) {ImageData data1 = this.list.get(i);ImageData data2 = this.list.get(i + 1);startX = data1.getRect().centerX();startY = data1.getRect().centerY();endX = data2.getRect().centerX();endY = data2.getRect().centerY();canvas.save();Paint paint = new Paint();paint.setStrokeWidth(3);paint.setStyle(Style.STROKE);paint.setAntiAlias(true);paint.setColor(Color.RED);canvas.drawLine(startX, startY, endX, endY, paint);canvas.restore();invalidate();if (isStopDrawStep && (recodeLineIndex != lineIndex)) {recodeLineIndex = lineIndex;isDrawStep = false;isStopDrawStep = false;}}if (isDrawLine && lineIndex <= this.list.size() - 2) {isDrawLine = false;addTask(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubhandler.obtainMessage(0).sendToTarget();semaphore.release();}});}}}}這樣就定義好了這個AnimationView,那麼在該怎麼使用呢?很簡單,可以加到布局檔案中,也可以簡單的在代碼中直接使用。
布局檔案中使用
fragment_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.animationview.MainActivity$PlaceholderFragment" > <com.example.animationview.AnimationView android:id="@+id/animationView" android:layout_width="match_parent" android:layout_height="match_parent" /></RelativeLayout>
package com.example.animationview;import android.graphics.Rect;import android.os.Bundle;import android.support.v7.app.ActionBarActivity;public class MainActivity extends ActionBarActivity {private AnimationView view;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.fragment_main);init();}private void init() {// TODO Auto-generated method stubview = (AnimationView) findViewById(R.id.animationView);view.addImageData(R.drawable.xin11, new Rect(0, 0, 100, 100));view.addImageData(R.drawable.xin16, new Rect(300, 150, 400, 250));view.addImageData(R.drawable.xin17, new Rect(300, 300, 500, 500));view.addImageData(R.drawable.xin2, new Rect(100, 200, 200, 300));view.addImageData(R.drawable.xin5, new Rect(100, 850, 200, 950));}}直接在代碼中使用
package com.example.animationview;import android.graphics.Rect;import android.os.Bundle;import android.support.v7.app.ActionBarActivity;public class MainActivity extends ActionBarActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);AnimationView view = new AnimationView(this);view.addImageData(R.drawable.xin11, new Rect(0, 0, 100, 100));view.addImageData(R.drawable.xin16, new Rect(300, 150, 400, 250));view.addImageData(R.drawable.xin17, new Rect(300, 300, 500, 500));view.addImageData(R.drawable.xin2, new Rect(100, 200, 200, 300));view.addImageData(R.drawable.xin5, new Rect(100, 850, 200, 950));setContentView(view);}}源碼下載
Android動態繪圖實現