首頁 > 軟體

Android事件處理的兩種方式詳解

2023-02-28 18:00:42

安卓提供了兩種方式的事件處理:基於回撥的事件處理和基於監聽的事件處理。

基於監聽的事件處理

基於監聽的事件處理一般包含三個要素,分別是:

Event Source(事件源):事件發生的場所,通常是各個元件

Event(事件):事件封裝了介面元件上發生的特定事件(通常就是使用者的一次操作)

Event Listener(事件監聽器):負責監聽事件源發生的事件,並對各種事件作出相應的響應

下面使用一個簡單的案例介紹按鈕事件監聽器

佈局檔案就是簡單的線性佈局器,上面是一個EditText,下面是一個Button按鈕

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal">
    <EditText
        android:id="@+id/txt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:editable="false"
        android:cursorVisible="false"
        android:textSize="12pt"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/bn"
        android:text="單擊我"/>
</LinearLayout>

使用Java程式碼給Button註冊一個事件監聽器

public class EventActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.event);
        Button bn = (Button) findViewById(R.id.bn);
        bn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText txt = (EditText) findViewById(R.id.txt);
                txt.setText("bn按鈕被單擊了!");
            }
        });
    }
}

單擊按鈕後,文字方塊就會顯示"bn按鈕被單擊了!"

外部類作為事件監聽器類

如果某個事件監聽器確實需要被多個GUI介面所共用,而且主要是完成某種業務邏輯的實現,那麼就可以考慮使用外部類形式來定義事件監聽器類。

我們定義一個類實現OnClickListener介面,並且實現onClick()方法

public class SendSmsListener implements View.OnClickListener {
    private Activity activity;
    private EditText address;
    private EditText content;
    public SendSmsListener(Activity activity, EditText adress, EditText content){
        Toast.makeText(activity, "初始化完成", Toast.LENGTH_SHORT).show();
        this.activity = activity;
        this.address = adress;
        this.content = content;
    }
    @Override
    public void onClick(View v) {
        String addressStr = address.getText().toString();
        String contentStr = content.getText().toString();
        // 獲取簡訊管理器
        SmsManager smsManager = SmsManager.getDefault();
        // 建立傳送簡訊想PendingIntent
        PendingIntent pendingIntent = PendingIntent.getBroadcast(activity, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE);
        // 傳送簡訊文字
        smsManager.sendTextMessage(addressStr, null, contentStr, pendingIntent, null);
        Toast.makeText(activity, "簡訊傳送完成", Toast.LENGTH_LONG).show();
    }
}

然後編輯一個簡單的線性佈局,有兩個輸入框和一個按鈕

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal">
    <EditText
        android:id="@+id/edit1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="50pt"/>
    <EditText
        android:id="@+id/edit2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="50pt"/>
    <Button
        android:id="@+id/send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="點選我傳送資訊"
        android:longClickable="true"/>
</LinearLayout>

最後編寫一個Activity

public class SendMessageActivity extends Activity {
    private EditText editText1;
    private EditText editText2;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.message);
        editText1 = (EditText) findViewById(R.id.edit1);
        editText2 = (EditText) findViewById(R.id.edit2);
        Button button = (Button) findViewById(R.id.send);
        button.setTextSize(25);
        button.setOnClickListener(new SendSmsListener(this, editText1, editText2));
        // 請求許可權
        ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.SEND_SMS} , 1);
    }
}

為了能夠順利傳送簡訊,需要重新開啟一臺模擬器,填寫模擬器的ID,否則程式會報錯

我的電腦設定沒有辦法同時執行兩個虛擬機器器,所以這裡就不展示了。

基於回撥的事件處理

回撥這個詞在程式設計領域經常被提及,我的理解是,回撥實際上是某個類中早已經定義好的方法或者介面,當我們繼承或者實現介面的時候,可以相應地重寫對應方法,或者實現相應介面。在程式執行的特定位置會呼叫特定的方法,當我們重寫了某個方法之後,就可以在特定情況下實現對應的邏輯。

最最簡單的一個例子就是Activity的onCreate()方法,當我們初始化一個Activity類的時候,就會呼叫這個方法,如果我們不重寫這個方法,那麼程式就會呼叫預設的onCreate()方法,如果我們重寫了這個方法,那麼程式就會呼叫我們重寫的onCreate()方法。

我們可以用回撥的方式實現一個跟隨手指的小球。

首先自定義一個自定義的View

public class DrawViewPlus extends View {
    public float currentX = 50;
    public float currentY = 50;
    // 定義建立畫筆
    Paint p = new Paint();
    public DrawViewPlus(Context context, AttributeSet set) {
        super(context, set);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 設定畫筆顏色
        p.setColor(Color.RED);
        // 繪製小球
        canvas.drawCircle(currentX, currentY, 15, p);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 獲取觸碰的座標點
        currentX = event.getX();
        currentY = event.getY();
        // 重新繪製小球
        this.invalidate();
        // 返回true表明處理方法已經處理完該事件
        return true;
    }
}

然後再xml檔案中加入自定義元件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- 使用自定義元件 -->
    <com.example.acitvitytest.ui.DrawViewPlus
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

在Activity中只需要簡單地載入介面就行,所有的邏輯都在自定義元件中編寫,這樣可以讓程式結構更加清晰。

public class DrawActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.draw);
    }
}

響應系統設定的事件

Configuration類專門用於描述手機裝置上的設定資訊,這些設定資訊既包括使用者特定的設定項,也包括系統的動態裝置設定。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="35pt"
        android:id="@+id/ori" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="35pt"
        android:id="@+id/navigation" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="35pt"
        android:id="@+id/touch" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="35pt"
        android:id="@+id/mnc" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/bn"
        android:textSize="35pt"
        android:text="獲取手機資訊"/>
</LinearLayout>
public class CfgActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.configuration);
        // 獲取應用介面中的介面元件
        EditText ori = findViewById(R.id.ori);
        EditText navigation = findViewById(R.id.navigation);
        EditText touch = findViewById(R.id.touch);
        EditText mnc = findViewById(R.id.mnc);
        Button bn = findViewById(R.id.bn);
        // 為按鈕繫結事件監聽器
        bn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 獲取系統的Configuration物件
                Configuration cfg = getResources().getConfiguration();
                String screen = cfg.orientation ==
                        Configuration.ORIENTATION_LANDSCAPE ? "橫向螢幕" : "縱向螢幕";
                String mncCode = cfg.mnc + "";
                String naviname;
                if(cfg.orientation == Configuration.NAVIGATION_NONAV){
                    naviname = "沒有方向控制";
                }
                else if (cfg.orientation == Configuration.NAVIGATION_WHEEL){
                    naviname = "滾輪控制方向";
                }
                else if (cfg.orientation == Configuration.NAVIGATION_DPAD){
                    naviname = "方向鍵控制方向";
                }
                else {
                    naviname = "軌跡球控制方向";
                }
                String touchname = cfg.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH ? "無觸控式螢幕" : "支援觸控式螢幕";
                ori.setText(screen);
                navigation.setText(naviname);
                touch.setText(touchname);
                mnc.setText(mncCode);
            }
        });
    }
}

點選按鈕就可以獲取相應的設定資訊

Handler訊息傳遞機制

Handler類的主要作用有兩個:在新啟動的執行緒中傳送訊息和在主執行緒中獲取處理訊息。

我們可以通過一個新執行緒來週期性地修改ImageView所顯示的圖片,通過這種方式來開發一個動畫效果。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/show"/>
</LinearLayout>
public class HanlderActivity extends Activity {
    // 定義週期性顯示圖片的id
    int[] images = new int[] {
            R.drawable.ic_launcher_foreground,
            R.drawable.ic_launcher_background
    };
    int currentImageId = 0;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.handler);
        ImageView image = findViewById(R.id.show);
        Handler myhandler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                // 如果訊息是本程式傳送的,那麼就修改ImageView顯示的圖片
                if(msg.what == 0x1233){
                    image.setImageResource(images[currentImageId++ % images.length]);
                }
            }
        };
        // 定義一個計時器,讓該計時器週期性地執行指定任務
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                // 傳送空訊息
                myhandler.sendEmptyMessage(0x1233);
            }
        },0, 1200);
    }
}

上述程式碼中使用TimerTask物件啟動了一個新的執行緒,由於啟動的執行緒沒有辦法直接存取Activity中的介面元件,因此使用Handler傳遞訊息,從而實現間接存取。程式會週期性地變換顯示的圖片

和Handler一起工作的元件有三個:

Message:Hanlder接收和處理的訊息物件

Looper:每個執行緒只能擁有一個Looper,他的loop方法負責讀取MessageQueue中的訊息,讀到資訊之後就把訊息交給傳送該訊息的Handler進行處理

MessageQueue:訊息佇列,他用先進先出的方式來管理Message

我們通常會將比較耗時的操作放到一個新的執行緒中去執行,如果使用UI執行緒執行耗時操作,那麼執行緒很可能被阻塞,從而降低使用者體驗。

我們可以看一下Looper物件中的prepare()方法

	/** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

這是一個靜態方法,大概的邏輯就是範例化一個Looper物件放到sThreadLocal容器中,並且容器中只能有一個Looper物件,假如在範例化前就已經存在了Looper物件,那麼就拋異常。

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

我們可以看到Looper物件的構造方法是用private修飾的,也就是說我們不能自己範例化Looper物件,只能通過呼叫靜態的prepare()方法進行構造。

最後構造得到的範例物件是放到ThreadLocal容器中的

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

Looper物件最重要的方法就是loop(),該方法會反覆檢查MessageQueue中是否有訊息,如果有訊息就會取出來進行處理,如果沒有訊息就會進行阻塞,直到取出訊息為止

/**
     * Poll and deliver single message, return true if the outer loop should continue.
     */
    @SuppressWarnings("AndroidFrameworkBinderIdentity")
    private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }
        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
        }
        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;
        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
        if (thresholdOverride > 0) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            if (me.mSlowDeliveryDetected) {
                if ((dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    me.mSlowDeliveryDetected = false;
                }
            } else {
                if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                        msg)) {
                    // Once we write a slow delivery log, suppress until the queue drains.
                    me.mSlowDeliveryDetected = true;
                }
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }
        msg.recycleUnchecked();
        return true;
    }
    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    @SuppressWarnings("AndroidFrameworkBinderIdentity")
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }
        me.mInLoop = true;
        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);
        me.mSlowDeliveryDetected = false;
        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

下面我們就寫一個簡單的程式計算到某個指定數為止的所有質數,並且用Toast顯示出來。

介面程式碼比較簡單,就是一個文字方塊和一個按鈕

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="50pt"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="點我進行計算"
        android:textSize="25pt"
        android:id="@+id/cal"
        android:onClick="cal"/>
</LinearLayout>

在Java程式碼中我們需要定義一個執行緒,裡面定義一個Handler類,該Handler類的處理訊息的邏輯是先從訊息中取出資料,然後進行計算,最後使用Toast顯示計算結果。

按鈕的點選事件的處理邏輯是,首先封裝一個Message物件,然後將Message物件傳遞給執行緒中的Handler物件。

public class CalNumActivity extends Activity {
    private final String UPPER = "UPPER_NUM";
    private CalThread calThread;
    private EditText editText;
    class CalThread extends Thread{
        public Handler mHandler;
        @Override
        public void run() {
            // 範例化Looper物件
            Looper.prepare();
            mHandler = new Handler(){
                @Override
                public void handleMessage(@NonNull Message msg) {
                    if(msg.what == 0x123){
                        // 獲取最大的那個數,從Message中取出Data
                        // 該Data是Bundle物件,採用鍵值對的形式傳遞資料
                        int upper = msg.getData().getInt(UPPER);
                        List<Integer> numlist = new ArrayList<>();
                        outer:
                        for(int i=2; i<=upper; i++){
                            for(int j=2; j<=Math.sqrt(i); j++){
                                // 只能被1和它本身整除的才是質數
                                if(j == i){
                                    continue;
                                }
                                if(j % i == 0){
                                    continue outer;
                                }
                            }
                            numlist.add(i);
                        }
                        // 顯示計算出來的質數
                        Toast.makeText(CalNumActivity.this, numlist.toString(), Toast.LENGTH_LONG).show();
                    }
                }
            };
            // 執行loop()方法,從MessageQueue中取出訊息
            Looper.loop();
        }
    }
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.calnum);
        editText = findViewById(R.id.input);
        Button button = findViewById(R.id.cal);
        // 啟動新執行緒
        calThread = new CalThread();
        calThread.start();
    }
    // 為按鈕的點選事件新增事件處理常式
    public void cal(View source){
        // 構建訊息
        Message msg = new Message();
        msg.what = 0x123;
        Bundle bundle = new Bundle();
        bundle.putInt(UPPER, Integer.parseInt(editText.getText().toString()));
        msg.setData(bundle);
        calThread.mHandler.sendMessage(msg);
    }
}

到此這篇關於Android事件處理的兩種方式詳解的文章就介紹到這了,更多相關Android事件處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com