1. 程式人生 > >Android進階:網路與資料儲存—步驟1:Android網路與通訊(第2小節:Handler)

Android進階:網路與資料儲存—步驟1:Android網路與通訊(第2小節:Handler)

內容概覽

  1. Handler是什麼
  2. 為什麼要使用Handler
  3. Handler/Looper/MessageQueue/Message
  4. Handler如何去實現(三種實現:1、下載檔案並更新進度條 2、倒計時 3、打地鼠的遊戲實現)
  5. 工作原理
  6. 如果更好的使用
  7. 擴充套件及總結

一、課程背景:

1、UI執行緒/主執行緒/AtivityThread

一個應用啟動會開啟一個主程序,接著這主程序會開啟一個主執行緒,所有東西會開始在主執行緒中運作

主執行緒也叫做UI執行緒,為什麼?因為主執行緒主要負責處理我們UI介面所有訊息相關的分發

2、執行緒不安全

執行緒安全不安全針對的事多執行緒的。

假如有個控制元件是button,多個執行緒都來更新這個button,這個button的狀態會不會混亂

執行緒安全就是button處理了這種多執行緒的狀態,給button加鎖,無論你哪個執行緒來方法都可以來訪問,這就是執行緒安全,但加鎖又導致效能的下降

執行緒不安全:沒有針對多執行緒進行特別處理;UI執行緒是執行緒不安全的

3、訊息迴圈機制(處理各種UI事件)

什麼意思?

也就是說我一進入主執行緒,就開始進入迴圈,這個迴圈是一個死迴圈,用來處理訊息、處理操作

當用戶在UI執行緒點了Button按鈕,它立馬將這個訊息發給這個迴圈,這個迴圈就開始處理

應用場景:

1.定時任務(訊息和可執行的物件在未來的一段時間內執行)

2.執行緒和執行緒之間的處理(A執行緒到B執行緒中執行某個動作)

二、Handler相關概念簡介

1.Handler

2.Looper(迴圈者)

3.Message

4.MessageQueue(訊息佇列)

寓意:Looper相當一個迴圈抽水機,MessageQueue相當於水井,message相當於水井裡的水

通過Looper不斷的抽出來到handlerMessage再交給handler處理者直接執行run;

三、使用Handler的實現和優化

3-1程式碼實現最簡單的Handler

1.Handler.sendMessage();

2.Handler.post();

首先,在主執行緒中新建一個handler物件實現他的sendMessage()方法,在這個方法裡面進行接受處理子執行緒傳來的更新UI的操作

子執行緒中進行耗時的操作,同時也想進行更新UI,就通過sendEmptyMessage();方法來將訊息傳遞給主執行緒的handler來處理

handler.sendEmptyMessage(1001);//1001是這個訊息的代號

主執行緒:

  //建立一個handler物件
        //實現它的handlerMessage方法
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //處理訊息
                Log.d(TAG, "handlerMessage:" + msg.what);

                /**
                 *主執行緒接到子執行緒發出來的訊息,處理
                 */
                if (msg.what == 1001)
                    //執行重新整理UI操作
                    textView.setText("我是代號1001的子執行緒訊息通過子執行緒發到主執行緒中處理");

            }
        };

子執行緒:

 //給button設定監聽事件
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //可能要做大量耗時操作
                /**
                 * 子執行緒
                 */
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(3000);//子執行緒休眠3秒,模仿耗時操作
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        /**
                         * 通知UI執行緒更新
                         */
                        handler.sendEmptyMessage(1001);

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

效果:

3-2Handler常見的傳送訊息方法

Handler.sendMessage(裡面裝的是一個訊息);message訊息進行打包,通過sendMessage()將訊息發給主執行緒

    //使用sendMessage()方法前的訊息要打包
                        Message message=Message.obtain();
                        message.what = 1002;//訊息編號
                        message.arg1 = 1003;
                        message.arg2 = 1004;
                        message.obj =MainActivity.this;//當前物件
                        handler.sendMessage(message);

主執行緒相當於收到一個打包好的快遞,進行拆包

  //建立一個handler物件
        //實現它的handlerMessage方法
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                /**
                 *主執行緒接到子執行緒發出來的訊息,處理
                 */
                if(msg.what ==1002){//拆包
                    Log.d(TAG, "handlerMessage:" + msg.what);
                    Log.d(TAG, "handlerMessage:" + msg.arg1);
                    Log.d(TAG, "handlerMessage:" + msg.arg2);
                    Log.d(TAG, "handlerMessage:" + msg.obj);
                    textView.setText("代號1002訊息通過子執行緒sendMessage方法發到主執行緒處理");
                }
            }
        };

定時任務:

  1. 定時傳送sendMessageAtTime();
  2. 延遲傳送sendMessageDelayed(); 
    //定時傳送訊息,時間是絕對的
     handler.sendMessageAtTime(message,SystemClock.uptimeMillis()+3000);
    //延遲傳送訊息,時間是相對的
     handler.sendMessageDelayed(message,2000);
                        

  post();方法 它可以直接在run函式中執行你想要做的事情(比如更新UI),他也有postDelayed()和postAtTime()方法

//提交訊息,可以直接在run裡面做你想做的事情
//更新UI操作轉到主執行緒進行

                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                int a=1+2+3;
                                TextView.setText("xxxxx");
                                ....
                            }
                        });

3-3非同步下載更新進度條

/**
 * 主執行緒——>start
 * 點選按鈕 |
 * 發起下載 |
 * 開啟子執行緒下載 |
 * 下載過程中通知主執行緒 |——>主執行緒更新UI
 *
 */

注意事項:

一、讀寫檔案的時候要獲取許可權

1.在AndroidManifest.xml中宣告許可權

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

2.android6.0之後要在動態申請許可權

    //android6.0之後要動態獲取許可權
    private void checkPermission(Activity activity) {
        // Storage Permissions
        final int REQUEST_EXTERNAL_STORAGE = 1;
        String[] PERMISSIONS_STORAGE = {
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE};

        try {
            //檢測是否有寫的許可權
            int permission = ActivityCompat.checkSelfPermission(DownLoadActivity.this,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) {
                // 沒有寫的許可權,去申請寫的許可權,會彈出對話方塊
                ActivityCompat.requestPermissions(DownLoadActivity.this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

全域性變數定義

    public static final int DOWNLOAD_MESSAGE_CODE = 1001;
    private static final int DOWNLOAD_MESSAGE_FAIL_CODE = 1000;
    public static final String appUrl = "http://download.sj.qq.com/upload/connAssitantDownload/upload/MobileAssistant_1.apk";
    private static Handler handler;
    private TextView textview;
    private int progress;
    public ProgressBar progressBar;

 二、主執行緒進行UI更新

        /**
         * 主執行緒進行UI更新
         * 進度條的更新
         */
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what) {
                    case DOWNLOAD_MESSAGE_CODE://下載發過來的msg.what(訊息編號1001)
                        progressBar.setProgress((Integer) msg.obj);
                        //textview.setText(msg.obj+"%");
                        break;
                    case DOWNLOAD_MESSAGE_FAIL_CODE://下載失敗時訊息編號為1000
                        Log.i("download", "fail");
                        Toast.makeText(DownLoadActivity.this,
                                "下載失敗!", Toast.LENGTH_SHORT)
                                .show();
                        break;
                }
            }
        };

三、子執行緒進行耗時操作(下載、網路請求)

  findViewById(R.id.button_download).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 子執行緒中進行下載
                 */
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        download(appUrl);

                    }
                }).start();//別忘了start()

            }
        });

四、下載的方法

    private void download(String appUrl) {
        try {
            URL url = new URL(appUrl);
            //開啟url物件的連結,獲得connection連結
            URLConnection urlConnection = url.openConnection();
            //獲取檔案的輸入流(讀取資料)
            InputStream inputStream = urlConnection.getInputStream();
            //獲取檔案的總長度
            int contentLength = urlConnection.getContentLength();
            //建立儲存位置,獲取sd卡的儲存路徑/storage/emulated/0/imooc/
            String downloadFolderName = Environment.getExternalStorageDirectory()
                    + File.separator + "imooc" + File.separator;
            //建立這個資料夾
            File file = new File(downloadFolderName);
            if (!file.exists()) {
                file.mkdirs();
            }
            //再上一個資料夾下又建立一個資料夾
            String fileName = downloadFolderName + "imooc.apk";
            File apkFile = new File(fileName);
            //如果已經有這個檔案了,就刪除重新下載過
            if (apkFile.exists()) {
                apkFile.delete();
            }
            //當前下載的長度處於檔案總長度就是progress
            int downloadSize = 0;
            //建立一個位元組陣列類似快取
            byte[] bytes = new byte[1024];

            int length = 0;
            //輸出流將資料寫入到這個檔案裡面
            OutputStream outputStream = new FileOutputStream(fileName);
            //從inputStream中讀取資料,當沒到檔案末尾
            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
                downloadSize += length;//第一次下載1024,第二次又是1024,累加
                /**
                 * 更新UI
                 */
                Message message = Message.obtain();
                //將當前的進度傳給主執行緒更新UI
                message.obj = downloadSize * 100 / contentLength;
                progress = downloadSize * 100 / contentLength;
                message.what = DOWNLOAD_MESSAGE_CODE;
                //1將訊息傳送給主執行緒
                handler.sendMessage(message);
                //2用runOnUiThread更新UI,也可以放到主執行緒裡面進行更新
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        textview.setText(progress + "%");
                        //下載完成提示
                        if (progress == progressBar.getMax()) {
                            Toast.makeText(DownLoadActivity.this,
                                    "下載完成!", Toast.LENGTH_SHORT)
                                    .show();
                        }
                    }
                });
            }
            inputStream.close();
            outputStream.close();

            //下載失敗也要發訊息
        } catch (MalformedURLException e) {
            notifyDownloadFail();
            e.printStackTrace();
        } catch (IOException e) {
            notifyDownloadFail();
            e.printStackTrace();
        }

    }
    //下載失敗也要將訊息發給主執行緒處理
    private void notifyDownloadFail() {
        Message message = Message.obtain();
        message.what = DOWNLOAD_MESSAGE_FAIL_CODE;
        //將訊息傳送給主執行緒
        handler.sendMessage(message);
    }

效果:

在/storage/emulated/0/imooc/裡下載好的imooc.apk

3-4倒計時實現

什麼是記憶體洩漏?

/**記憶體洩漏:
 * Handler通常會伴隨著一個耗時的後臺執行緒(例如從網路拉取圖片)一起出現,
 * 這個後臺執行緒在任務執行完畢(例如圖片下載完畢)之後,
 * 通過訊息機制通知Handler,然後Handler把圖片更新到介面
 * 然而,如果使用者在網路請求過程中關閉了Activity
 * 正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由於這時執行緒尚未執行完
 * 該執行緒該執行緒持有Handler的引用,這個Handler又持有Activity的引用,
 * 依然持有Activity的引用(TextView)countdowntime
 * 所以Activity關閉後,就導致該Activity無法被GC回收(即記憶體洩露)
 */

方法1:直接使用Handler來處理,缺點:容易造成記憶體洩漏

         
           Handler handler=new Handler(){
            @Override
            public void handleMessage(Message msg) {
                 super.handleMessage(msg);
                //迴圈發訊息控制
                if(msg!=null&&msg.what==COUNDOWN_TIME_CODE){
                    int value=9;
                    countdowntime.setText(value--);
               //重寫構造一個message,重新發回去(不能直接用msg,否則會閃退)
                        Message message = Message.obtain();
                        message.what = COUNDOWN_TIME_CODE;
                        message.arg1 = value;
                        sendMessageDelayed(message, 1000);
                }
            }
        };

     //建立一個靜態的Handler,不會發生記憶體洩漏
        Handler handler = new Handler(this);
        Message message = Message.obtain();//從訊息池裡面拿訊息
        message.what = COUNDOWN_TIME_CODE;//訊息編號
        message.arg1 = MAX_COUNT;
        //第一次發生message將訊息延遲1s傳送
        handler.sendMessageDelayed(message, DELAY_MILLIS);

    }

方法2://建立一個靜態的Handler,不會發生記憶體洩漏

  弱引用:

  • 可以通過弱引用拿到我們的Activity,再通過Acitivity拿到控制元件
  •  不會記憶體洩漏的原因
  •  因為這個MainActivity是弱引用的
  •  當我們持有它,用完之後就會被回收了
public class MainActivity extends AppCompatActivity {

    public static final int COUNDOWN_TIME_CODE = 1001;//倒計時handler code
    public static final int DELAY_MILLIS = 1000;//倒計時間隔
    public static final int MAX_COUNT = 10;//倒計時最大值
    private TextView countdowntime;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //得到控制元件
        countdowntime = findViewById(R.id.textview);

        //建立一個靜態的Handler,不會發生記憶體洩漏
        CountdownTimeHandler handler = new CountdownTimeHandler(this);
        Message message = Message.obtain();//從訊息池裡面拿訊息
        message.what = COUNDOWN_TIME_CODE;//訊息編號
        message.arg1 = MAX_COUNT;
        //第一次發生message將訊息延遲1s傳送
        handler.sendMessageDelayed(message, DELAY_MILLIS);
    }

    //直接寫一個靜態的Handler
    public static class CountdownTimeHandler extends Handler {
        //弱引用,可以通過弱引用拿到我們的Activity,再通過Acitivity拿到控制元件
        final WeakReference<MainActivity> mWeakReference;

        //預設構造方法 command+N 在構造方法中初始化
        public CountdownTimeHandler(MainActivity activity) {
            /**不會記憶體洩漏的原因
             * 因為這個MainActivity是弱引用的
             * 當我們持有它,用完之後就會被回收了
             */
            //獲取當前Activity的弱引用
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //拿到弱引用的MainActivity
            MainActivity activity = mWeakReference.get();
            switch (msg.what) {
                case COUNDOWN_TIME_CODE:
                    int value = msg.arg1;
                    //拿到textview
                    activity.countdowntime.setText(String.valueOf(value--));

                    //迴圈發訊息控制
                    if (value >= 0) {
                        //重寫構造一個message,重新發回去(不能直接用msg,否則會閃退)
                        Message message = Message.obtain();
                        message.what = COUNDOWN_TIME_CODE;
                        message.arg1 = value;
                        sendMessageDelayed(message, 1000);
                    }


            }
        }


    }
}

結果:

3.5-打地鼠遊戲的實現(改)

思路:第一次傳送遊戲開始訊息,之後由自定義的靜態Hanlder處理,然後由它迴圈傳送訊息編號一致的訊息,它自己接受後處理

程式碼:DigLetAcitivity.java

import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.lang.ref.WeakReference;
import java.util.Random;

public class DigLetActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {

    private static final int RANDOM_NUMBER = 500;
    private final int START_TIME = 3;//開始倒計時
    private TextView resultTextView, start_timeTv;
    private ImageView diglettImageView;
    private Button start_button;
    //隨機生成地鼠的位置
    public int[][] mPosition = new int[][]{
            {342, 180}, {432, 880}, {332, 110},
            {782, 140}, {466, 380}, {732, 900},
            {52, 180}, {562, 80}, {72, 80},
            {342, 180}, {432, 880}, {332, 110},
            {882, 50}, {652, 130}, {332, 110},
            {452, 90}, {132, 340}, {532, 670},
            {342, 100}, {132, 280}, {432, 610},
            {92, 60}, {832, 880}, {762, 90},
    };
    private int mTotalCount;//所有地鼠的數量
    private int mSuccessCount;//成功的數量
    //初始化handler
    private DiglettHandler mHandler = new DiglettHandler(this);
    //全域性變數
    public static final int MAX_COUNT = 100;
    public static final int NEXT_CODE = 123;
    public static final int START_TIME_CODE = 1234;//倒計時編號
    private Button start_again;
    private static int startTime;
    private ImageView girlImageView;
    private Vibrator vibrator;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dig_let);
        setTitle("Kiss Game");
        initView();
        //點選圖片手機震動
        vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

    }

    private void initView() {
        start_timeTv = findViewById(R.id.start_textview);
        resultTextView = findViewById(R.id.text_view);
        diglettImageView = findViewById(R.id.image_view_pig);
        girlImageView = findViewById(R.id.image_view_girl);
        start_again = findViewById(R.id.button_again);
        start_button = findViewById(R.id.start_button);
        start_button.setOnClickListener(this);
        start_again.setOnClickListener(this);
        diglettImageView.setOnTouchListener(this);//圖片點選事件
        girlImageView.setOnTouchListener(this);//圖片失敗點選事件
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.start_button://開始按鈕
                start();
                break;
            case R.id.button_again://重開按鈕
                clear();
                break;
        }

    }

    //第一次傳送訊息
    private void start() {
        //傳送訊息hander.sendMessageDelayer
        //初始化
        startTime();//倒計時
        resultTextView.setText("Start~");
        start_button.setText("Playing..");
        start_button.setEnabled(false);//設定按鈕不能再按了
        next(0);

    }

    //倒計時
    private void startTime() {
        Message message = Message.obtain();
        message.what = START_TIME_CODE;//訊息編碼
        message.arg2 = START_TIME;//倒計時的數
        //發訊息
        mHandler.sendMessageDelayed(message, 1000);
    }

    //這隻打完,下一隻出來
    private void next(int delayTime) {
        //生成地鼠個數,隨機選
        int position = new Random().nextInt(mPosition.length);

        Message message = Message.obtain();
        message.what = NEXT_CODE;//訊息編碼
        message.arg1 = position;//發隨機地鼠的位置
        //發訊息
        mHandler.sendMessageDelayed(message, delayTime);
        mTotalCount++;//每次執行一次next總數累加

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.image_view_pig:
                v.setVisibility(View.GONE);//當地鼠被點選後,設定為不可見
                mSuccessCount++;//成功點選的數量
                break;
            case R.id.image_view_girl:
                vibrator.vibrate(100);//手機震動100毫秒
                v.setVisibility(View.GONE);//當地鼠被點選後,設定為不可見
                mSuccessCount--;//點選失敗的數量

        }

        resultTextView.setText(" Kiss: " + mSuccessCount + " Times");

        return false;
    }

    //新建一個靜態Handler,防止記憶體洩漏
    public static class DiglettHandler extends Handler {
        //弱引用
        public final WeakReference<DigLetActivity> mWeakReference;

        public DiglettHandler(DigLetActivity activity) {
            mWeakReference = new WeakReference<>(activity);
        }

        //接受到訊息進行處理
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //拿到弱引用的Activity
            DigLetActivity activity = mWeakReference.get();

            switch (msg.what) {
                case START_TIME_CODE:
                    //拿到倒計時時間
                    startTime = msg.arg2;
                    if (startTime > 0) {
                        //顯示出來
                        activity.start_timeTv.setText(String.valueOf(startTime--));
                        //重寫構造一個message,重新發回去(不能直接用msg,否則會閃退)
                        Message message = Message.obtain();
                        message.what = START_TIME_CODE;
                        message.arg2 = startTime;
                        sendMessageDelayed(message, 1000);
                    } else {
                        activity.start_timeTv.setText(String.valueOf(startTime--));
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    Thread.sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                        activity.start_timeTv.setVisibility(View.GONE);
                    }

                case NEXT_CODE:
                    //停止條件
                    if (activity.mTotalCount > MAX_COUNT) {
                        activity.clear();
                        Toast.makeText(activity, "Game Over", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    if (startTime < 0) {
                        //拿到座標
                        int position = msg.arg1;
                        //設定圖片的X座標(二維陣列)0是第一個:X。1是第二個:Y
                        activity.diglettImageView.setX(activity.mPosition[position][0]);
                        activity.diglettImageView.setY(activity.mPosition[position][1]);
                        //讓圖片可見
                        activity.diglettImageView.setVisibility(View.VISIBLE);
                        //設定圖片的X座標(二維陣列)0是第一個:X。1是第二個:Y
                        activity.girlImageView.setX(activity.mPosition[position][1]);
                        activity.girlImageView.setY(activity.mPosition[position][0]);
                        //讓圖片可見
                        activity.girlImageView.setVisibility(View.VISIBLE);
                        //生成隨機時間毫秒為單位
                        int randomTime = new Random().nextInt(RANDOM_NUMBER) + RANDOM_NUMBER;
                        activity.next(randomTime);//將時間傳給下一個//
                    }
                    break;
            }

        }
    }

    //清空
    public void clear() {
        mTotalCount = 0;
        mSuccessCount = 0;
        diglettImageView.setVisibility(View.GONE);
        startTime = 3;
        start_timeTv.setText("");
        start_timeTv.setVisibility(View.VISIBLE);
        resultTextView.setText("");
        start_button.setText("Start");
        start_button.setEnabled(true);
    }

}

滑鼠跟蹤圖片效果: LockScreenView.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;

public class LockScreenView extends ImageView {
    public float currentX = 40;
    public float currentY = 50;
    private Bitmap bmp;

    public LockScreenView(Context context) {
        super(context);
        init();
    }

    public LockScreenView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LockScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    //初始化你需要顯示的游標樣式
    private void init() {

        if (bmp == null) {
            bmp = BitmapFactory.decodeResource(getResources(), R.drawable.kissyou);
        }
    }

    private boolean isClickView = false;//標識是否是人為點選,是則為true

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isClickView == true && bmp != null) {
            //建立畫筆
            Paint p = new Paint();
            canvas.drawBitmap(bmp, currentX - (bmp.getWidth() / 2), currentY - (bmp.getHeight() / 2), p);
            isClickView = false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //當前元件的currentX、currentY兩個屬性
        this.currentX = event.getX();
        this.currentY = event.getY();
        isClickView = true;

        if (event.getAction() == MotionEvent.ACTION_UP && bmp != null) {
            this.currentX = -bmp.getWidth();
            this.currentY = -bmp.getHeight();
            isClickView = false;
        }
        //通知改元件重繪
        this.invalidate();
        //返回true表明處理方法已經處理該事件
        return false;
    }
}

點錯手機震動效果:

1、AndroidManifest.xml獲取許可權

    <uses-permission android:name="android.permission.VIBRATE"/>

2、 建立物件

    private Vibrator vibrator;
 //點選圖片手機震動
        vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

點選事件中新增震動時間 

                vibrator.vibrate(100);//手機震動100毫秒