Android開發高仿Last App Switcher小程式執行個體

來源:互聯網
上載者:User

在Android眾多工具類app中,Last App Switcher絕對算是一個讓人用過就不會卸載的工具。LAS這個應用,它的功能很簡單,就是通過一個浮動按鈕實現在兩個應用之間一鍵切換,但是非常實用,尤其是在邊玩邊聊天需要頻繁切換應用的時候。所以可以看出,想開發一款受歡迎的應用,一定要注重使用者體驗,只要使用者用的爽,功能再再再簡單,它也會受歡迎。那麼這功能到底有多簡單呢?跟我來實現一下就好了。


我就不截圖了,下面用官方的截圖來說明。這裡真心推薦讀者下載用一下。Google商店的下載地址:Last App Switcher 搞開發的應該都會FQ吧

看下原始程式介面:

可以看到主介面就是一系列開關選項,同時程式右邊有一個浮動的圓形視窗。下面我會按照步驟一步步增加功能。


仿iOS按鈕

寫demo不需要多好的介面,但也不能太醜,手裡有看起來不錯的控制項就直接拖進來用了。下面是效果圖,這一套按鈕有好幾種,都是仿iOS的,想要的可以點原作者的這篇部落格,源碼Github地址。

先添加一個開關主功能的按鈕:


浮動按鈕

可以看到,這個應用的主要功能就在於那個紅色的浮動按鈕上面。根據程式功能可以知道,這個浮動按鈕是由程式開啟的服務中建立的。又因為程式的Activity在離開onStart()狀態後就會銷毀(這樣做的原因後面說),之後按鈕仍保持其可用狀態。所以可以知道是通過startService()啟動的服務。下面我們就需要先寫一個服務出來,再在服務中繪一個浮動按鈕。具體有關服務的細節參考我上一篇部落格:部落格傳送門。

寫一個服務FloatButtonService,在AndroidManifest.xml檔案添加服務

<service android:name=".FloatButtonService" >
</service>

服務中添加繪製浮動按鈕方法,相關說明見注釋

private void createFloatView() {

}

方法添加完畢在服務相應的調用位置建立和銷毀浮動按鈕

@Override
public void onCreate() {
    // TODO Auto-generated method stub
    super.onCreate();
    createFloatView();
}

@Override
public void onDestroy() {
    // TODO Auto-generated method stub
    super.onDestroy();
    if (mFloatLayout != null) {
        mWindowManager.removeView(mFloatLayout);
    }
}

使用浮動按鈕還需要增加許可權:

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

這樣,我們在MainActivity中就可以為按鈕增加響應事件,進行開啟和關閉服務了。

將程式從最近任務(last recent tasks)中移除

  按下系統導覽列第三個按鈕我們就可以看到最近使用過的工作清單,當然,LAS切換程式也是在這裡選擇最後使用的兩個應用程式切換的。所以在切換的時候,把自己的Activity從最近的任務中刪掉是很必要的。
前面提到過,就是在Activity的onPause()狀態或者onStop()狀態中執行finishAndRemoveTask()方法刪除任務。但這個方法在API 21也就是Android 5.0才引入。不過,我們還有一個更方便的方法,就是在設定檔的<activity>標籤中增加

android:excludeFromRecents="true"

這樣不論你是按下back鍵還是home鍵,程式都會從最近使用過的工作清單中刪除

任務間的切換

  將自身Activity從最近工作清單中刪除後,我們就可以考慮擷取最後兩次的任務,然後互相一鍵切換了。
在浮動按鈕的單擊事件中添加
首先需要獲得ActivityManager的對象

ActivityManager mActivityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);

要擷取任務還需要對應許可權

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

有了許可權,就可以擷取到工作清單了

List<ActivityManager.RecentTaskInfo> mAppList = new ArrayList<ActivityManager.RecentTaskInfo>();
mAppList = mActivityManager.getRecentTasks(3, ActivityManager.RECENT_IGNORE_UNAVAILABLE);

建立一個裝有RecentTaskInfo的列表,通過getRecentTasks方法擷取系統的最近使用過的應用列表。

關於getRecentTasks方法,第一個參數是一個整型值,是你需要返回的應用數量,但實際上得到的數量可能會比這個值要小。比如
我要得到3個,但後台只開了1個,那麼只返回1個。第二個參數是要返回的應用的狀態,我選擇的是忽略停用應用,應該是完全關閉,不在背景應用。

再說一點,這個方法在Android5.0因為安全問題屏蔽掉了,也就是android5.0以上的版本不能用這個方法。所以我前一陣子在App Store上看到評論都是Android5.0用這個沒有效果。現在行不行我倒不知道,閑了再研究吧。(每次我說閑了再做基本都是個坑- -|)

前面的參數我之所以要選擇3,是因為我只需要獲得最近使用的2個應用,因為每次開新應用,這個應用資訊都會存在列表的最上面,所以擷取前3個即可。

但為什麼是3而不是2呢,因為Android的Home介面也是一個Activity(應該是),我可以選擇是否要在切換的時候忽略掉Home介面。所以考慮到Home,就要用3。
Home的包名為com.android.launcher,以此為根據進行判斷即可。

private void getAndStartIntent(int i){
    ActivityManager.RecentTaskInfo info = mAppList.get(i);
    if (null == info)
        Toast.makeText(FloatButtonService.this, "No other apps", Toast.LENGTH_SHORT).show();
    else if(sp.getBoolean(StringKey.ExcludeHome, false)){ // if set true, do follow func
        if(info.baseIntent.getComponent().getPackageName().equals(HOME_PACKAGE))    //exclude HOME
            getAndStartIntent(2);
    }else
        startActivityWithoutAnimation(info);
}

啟動一個應用的過程預設是有一個切換動畫的,我們的程式就是用來切換程式的,所以取消啟動動畫是一個比較好的選擇。
只用給要啟動的intent加一個flag即可(有些情況下不會生效)

intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);


開機啟動

  Android開機啟動結束會發送一個BOOT_COMPLETED的廣播,我們在程式中建立一個廣播接收器來接收這個廣播,接收成功就直接啟動服務來顯示浮動按鈕即可。
先建立一個廣播接收器 BootReceiver

public class BootReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO Auto-generated method stub
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {// on boot
            Intent a = new Intent(context, FloatButtonService.class);
            context.startService(a);
        }
    }
}

在設定檔中,<application>標籤下註冊廣播接收器

<receiver android:name=".BootReceiver" >
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</receiver>

然後增加許可權

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

開機啟動就完成了。但怎麼用開關來控制其是否開機啟動呢?

SharedPreferences

  用開關控制功能的開啟狀態,這個狀態不能儲存在程式中,因為程式是要被關閉的。那麼就是要用一些方法儲存開關的狀態到系統中,然後服務從檔案讀取狀態,控制自己的程式行為。Android中最適合儲存配置狀態的就是用SharedPreferences了。當我查看LAS應用的資料檔案的時候,發現輸出的結果的確是這樣的。

cat /data/data/com.abhi.lastappswitcher/shared_prefs/com.inpen.lastAppSwitcher.APPLICATION_PREFS.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="com.inpen.lastAppSwitcher.PREF_SNAP_TO_EDGE" value="true" />
<int name="com.inpen.lastAppSwitcher.PREF_LAND_HEIGHT" value="800" />
<int name="com.inpen.lastAppSwitcher.PREF_LAND_FLOATER_Y" value="485" />
<int name="com.inpen.lastAppSwitcher.PREF_LAND_WIDTH" value="1280" />
<int name="com.inpen.lastAppSwitcher.PREF_PORT_FLOATER_Y" value="776" />
<int name="com.inpen.lastAppSwitcher.PREF_PORT_WIDTH" value="800" />
<boolean name="com.inpen.lastAppSwitcher.PREF_ERROR_MSG" value="true" />
<boolean name="com.inpen.lastAppSwitcher.PREF_STATUS_BAR_OVERLAY" value="false" />
<int name="com.inpen.lastAppSwitcher.PREF_FLOATER_SIZE" value="55" />
<int name="com.inpen.lastAppSwitcher.PREF_PORT_FLOATER_X" value="765" />
<int name="com.inpen.lastAppSwitcher.PREF_PORT_HEIGHT" value="1280" />
<int name="com.inpen.lastAppSwitcher.PREF_FLOATER_TRANSPARENCY" value="75" />
<int name="currentQuote" value="6" />
<int name="com.inpen.lastAppSwitcher.PREF_SWITCHING_METHOD" value="1" />
<boolean name="com.inpen.lastAppSwitcher.PREF_FLOATER_MOVABLE" value="true" />
<boolean name="com.inpen.lastAppSwitcher.PREF_HAPTIC_FEEDBACK" value="false" />
<int name="com.inpen.lastAppSwitcher.PREF_FLOATER_COLOR" value="0" />
</map>

那麼,我們就可以根據自己的需求來寫sharedPreferences檔案了
先獲得 SharedPreferences 的執行個體

SharedPreferences sp = getSharedPreferences("las_demo", Context.MODE_PRIVATE);

參數1是不帶尾碼的檔案名稱,根據檔案名稱擷取執行個體,同一個名字的SharedPreferences對象只獲得同一個執行個體;
參數2是模式操作模式:

    Context.MODE_PRIVATE:
    為預設操作模式,代表該檔案是私人資料,只能被應用本身訪問,在該模式下,寫入的內容會覆蓋原檔案的內容。
    Context.MODE_APPEND:
    建立的檔案是私人資料,該模式會檢查檔案是否存在,存在就往檔案追加內容,否則就建立新檔案。
    MODE_WORLD_READABLE:
    表示當前檔案可以被其他應用讀取。
    MODE_WORLD_WRITEABLE:
    表示當前檔案可以被其他應用寫入。

獲得執行個體之後要進行初始化,寫入一些設定值。這裡因為初始化只需要一次,但我沒找到判斷sharedPreferences檔案是否存在的方法(沒想用File去查,這個檔案存在系統路徑,有許可權問題,估計不行,有知道的可以告訴我),有一個public abstract boolean contains (String key)方法,但用了感覺沒效果,所以我又加了一個key,來儲存第一次建立的狀態,然後寫入其他鍵-值,儲存。

if(!sp.getBoolean(StringKey.FirstCreate, true)){

    Editor editor = sp.edit();
    editor.putBoolean(StringKey.FirstCreate, true);
    editor.putBoolean(StringKey.RunAtStart, false);
    editor.putBoolean(StringKey.SnapToEdge, true);
    editor.putBoolean(StringKey.StatusBarOverlay, false);
    editor.putBoolean(StringKey.ExcludeHome, true);

    editor.commit();
}

設定好鍵-值後就可以根據這些值設定介面裡按鈕的開關狀態和設定程式的一些行為。

if (sp.getBoolean(StringKey.RunAtStart, false))
    mBtnRunAtStartup.setChecked(true);
else
    mBtnRunAtStartup.setChecked(false);

當然,在手動改變按鈕狀態的時候也要為某個key重新寫入新的value

mBtnRunAtStartup.setOnCheckedChangeListener(new OnCheckedChangeListener() {

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        // TODO Auto-generated method stub
        if (isChecked)
            editBoolKey(StringKey.RunAtStart, true);
        else
            editBoolKey(StringKey.RunAtStart, false);
    }
});


private void editBoolKey(String str, boolean b) {
    Editor editor = sp.edit();
    editor.putBoolean(str, b);
    editor.apply();
}

改SharedPreferences的key-value的時候需要獲得editor對象執行個體,設定完成用apply()方法或者commit()方法提交修改。如果有兩個editor執行個體在同時修改,則以最後一次的提交為準。如果不關心傳回值,且在應用的主線程裡使用,用apply()要比commit()好。
至此,需要開關功能就在功能實現的地方加一層讀取SP索引值的過程,根據讀到的結果決定功能。是否可用。

懸浮按鈕顯示在status bar上方

按照下面設定windowManager的屬性就好,沒什麼好解釋的,放上文檔看吧。

wmParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
               | LayoutParams.FLAG_NOT_FOCUSABLE
               | LayoutParams.FLAG_LAYOUT_IN_SCREEN;

    int android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL = 32 [0x20]
    Window flag: Even when this window is focusable (its is not set), allow any pointer events outside of the window to be sent to the windows behind it. Otherwise it will consume all pointer events itself, regardless of whether they are inside of the window.

    int android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE = 8 [0x8]
    Window flag: this window won't ever get key input focus, so the user can not send key or other button events to it. Those will instead go to whatever focusable window is behind it. This flag will also enable FLAG_NOT_TOUCH_MODAL whether or not that is explicitly set.
    Setting this flag also implies that the window will not need to interact with a soft input method, so it will be Z-ordered and positioned independently of any active input method (typically this means it gets Z-ordered on top of the input method, so it can use the full screen for its content and cover the input method if needed. You can use FLAG_ALT_FOCUSABLE_IM to modify this behavior.

    int android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN = 256 [0x100]
    Window flag: place the window within the entire screen, ignoring decorations around the border (a.k.a. the status bar). The window must correctly position its contents to take the screen decoration into account. This flag is normally set for you by Window as described in Window.setFlags.


按鈕邊緣吸附效果

  這個應該是最簡單的了,在按鈕的touch事件中,當移動結束,手指抬起行為ACTION_UP中對位置進行判斷,如果按鈕的x座標在螢幕左半邊,x設為0,即貼著螢幕左邊緣顯示,反之一個道理。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.