分類
Android

Firebase Cloud Messaging 使用紀錄

Firebase cloud messaging(FCM) 是什麼?

FCM 是一個跨平台的訊息傳送機制,提供可靠且免費傳遞訊息的功能。

如何使用 FCM?

在已經建立 Firebase 專案的情況下,啟動並使用 FCM 的過程非常簡單。

Android App 只需要加入一個 Service,Firebase 後臺只需要啟動 FCM 並建立要傳送的訊息,最後透過 Firebase 後台發送訊息,Android App 便可收到該訊息。

以下分為 Android App 端,以及 Firebase 後台端來說明。

首先是 Android App 端。

先加入 FCM 的依賴,在 Module 的 build.gradle 加入以下內容

implementation 'com.google.firebase:firebase-messaging:20.0.0'

接著在 AndroidManifest.xml 宣告 Service 如下,Service 名稱可以自訂,注意 <intent-filter> 的 <action> 的 android:name 的內容為 com.google.firebase.MESSAGING_EVENT

<service
android:enabled="true"
android:exported="true"
android:name=".notification.FirebaseMessagingReceiveService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

接著來實作 Service,如下

package com.codefoxx.firebasedemo1app.notification
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class FirebaseMessagingReceiveService : FirebaseMessagingService() {
    private val TAG = this::class.java.simpleName
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        Log.d(TAG, "receive notification:" + remoteMessage.notification?.body)
    }
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d(TAG, "onNewToken: " + token)
    }
}

Android App端設定完成!! 這樣便可收到訊息。

接下來是 Firebase 後台端

請先進入 Firebase 專案,進入專案之後如果是第一次使用 FCM 必須先建立網路推播憑證。步驟如下

1.點擊 Settings -> 專案設定
(Firebase 專案頁面左上方有 Project Overview,Project Overview 旁邊有一個齒輪圖示,該圖示就是 Settings)

2.點擊雲端通訊

3.在雲端通訊最下方有網路設定,其中有網路推播憑證,在該區塊中若沒有任何內容,請新增一組金鑰。

接下來就可以建立要發送訊息的內容了。

首先點擊畫面左側欄的 Cloud Messaging,再點擊新增通知就會進入訊息內容設定。

1.通知: 填寫訊息的Title和內容

2.指定目標: 選擇使用者區隔或主題
使用者區隔就是選擇符合的條件,其中的應用程式選擇發送目標 App 的 package name,代表所有安裝該目標 App 的裝置都會收到訊息。可以再點擊後方的且,選擇其他的條件。

主題是一個關鍵字,在 App 中必須先訂閱該主題才會發送到目標裝置。

3.排定時間: 就是發送訊息的時間,預設為現在,若有需求可以選擇其他項目。

以上為必選的訊息設定,以下的設定轉換事件和其他選項為選用,視需求來決定要不要使用。

完成後點擊審查會出現訊息提示內容,再按下發布就會發送訊息了。

分類
Android

View Binding

View Binding 是什麼 ?

View Binding 是一個機制,可以讓開發者更簡單以及更安全的參考佈局檔的 UI 元件。
ref : https://developer.android.com/topic/libraries/view-binding

當 Module 啟動 View Binding 之後,該 Module 內每個佈局檔都會產生一個相對應的 binding class,binding class 會包含其對應的佈局檔中具有 id 的屬性,使用者便可透過 binding class 直接操作佈局檔的 UI 元件,不必再寫 findViewById。

View Binding 用來做什麼 ?

在大部分的情況下,View Binding 用來取代 findViewById。

設定和相依性

注意
1.目前(2019-09-12)只支援 Android Studio 3.6 Canary 11+
2.如果已經使用 data binding,不必再啟動 View Binding

在 Module 的 build.gradle 啟動 View Binding

android {
    ...
    viewBinding {
        enabled = true
    }
}

如果不想為某個佈局檔自動產生 binding class,在該佈局檔中加入以下設定

<LinearLayout
        ...
        tools:viewBindingIgnore="true" >
    ...
</LinearLayout>

用法

在 Module 中啟動 View Binding 之後,只要執行 make project,便會為該 Module 中每個佈局檔建立 binding class。

binding class 檔案位於
app -> build -> generated -> data_binding_base_class_source_out -> debug -> out -> package name -> databinding
(這個其實不重要,因為不必手動修改它)

binding class 名稱會根據對應的佈局檔延伸,規則是佈局檔+Binding
假設有一個為 activity_view_binding_example.xml 佈局檔,內容如下。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ViewBindingExampleActivity">
    <TextView
        android:id="@+id/view_binding_example_hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/view_binding_example_click_me"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click me"
        app:layout_constraintLeft_toLeftOf="@+id/view_binding_example_hello_world"
        app:layout_constraintTop_toBottomOf="@+id/view_binding_example_hello_world" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="bottom button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

可以看到其中一個 TextView 和 Button 都有 id,而另一個 Button 沒有 id。
binding class 會自動持有具有 id 屬性的 UI 元件變數,忽略掉沒有 id 屬性的UI 元件。

對應的 binding class 名稱為 ActivityViewBindingExampleBinding。它會持有名為 viewBindingExampleClickMe 變數以及 viewBindingExampleHelloWorld 變數。這兩個變數對應佈局檔的

<TextView
 android:id="@+id/view_binding_example_hello_world"
...
/>
<Button
 android:id="@+id/view_binding_example_click_me"
...
/>

只要將 ViewBinding 初始化之後就可以透過這兩個變數來操作佈局檔的 UI 元件,以下為在 Activity 中使用的範例。

class ViewBindingExampleActivity : AppCompatActivity() {
    private lateinit var mViewBinding: ActivityViewBindingExampleBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewBinding = ActivityViewBindingExampleBinding.inflate(layoutInflater)
        setContentView(mViewBinding.root)
        initUI()
    }
    private fun initUI() {
        mViewBinding.viewBindingExampleClickMe.setOnClickListener { _ ->
            Toast.makeText(this, "change to NEW hello world!!!", Toast.LENGTH_SHORT).show()
            mViewBinding.viewBindingExampleHelloWorld.text = "new hello world"
        }
    }
}

mViewBinding 就是 binding class 的實體變數。

注意初始化 mViewBinding 的呼叫順序部分。初始化完 mViewBinding 之後呼叫setContentView 方法是傳入 mViewBinding 的 root 方法回傳值。
root 方法其實就是回傳對應佈局檔的根元素,本範例就是 ConstraintLayout。

        mViewBinding = ActivityViewBindingExampleBinding.inflate(layoutInflater)
        setContentView(mViewBinding.root)

而在 initUI 方法中就是透過 mViewBinding 操作 UI 元件,不須經過初始化也不用 findViewById。

    private fun initUI() {
        mViewBinding.viewBindingExampleClickMe.setOnClickListener { _ ->
            Toast.makeText(this, "change to NEW hello world!!!", Toast.LENGTH_SHORT).show()
            mViewBinding.viewBindingExampleHelloWorld.text = "new hello world"
        }
    }

優勢

View Binding 透過提供 binding class 的實體來操作 UI 元件,這種方式比 findViewById 多了一些好處(就是不用寫 findViewById!!)。

首先為型態安全,因為不必在意呼叫 findViewById 時轉型的型態是否正確。
第二為 Null 安全,因為不必在意呼叫 findViewById 時指定 UI 元件的 ID。

和 Data Binding 的不同

  • Data Binding 只處理使用<layout>標籤來綁定數據。
  • View Binding 無法處理在佈局檔定義的變數以及表達式。

總結

就目前而言 View Binding 是很簡易的新特性,用途有限,主要用於取代 findViewById,其 binding class 提供的方法也不多。

分類
Android

Android 10 on Pixel 3 使用心得

使用 Pixel 好處就是能在第一時間收到 Android 的更新。

Android Developers 在 Twitter上很早( 2019-09-04 1:24 )就發布消息,沒想到20190904 下午就可以更新了。

可更新消息

這次更新檔不小,足足有 1.3G 多。更新的主要內容有 10 項,分別為

  • Live Caption
  • Smart Reply
  • Sound Amplifier
  • Gesture Navigation
  • Dark theme
  • Privacy Controls
  • Location Controls
  • Security Updates
  • Focus mode
  • Family Link

詳細內容在 https://www.android.com/android-10/ ,以下為總結內容。

Live Caption

可以當作 Youtube 播放字幕的加強版,特別的是在沒有網路連線的情況下也可以進行字幕播放。

目前還未開放(2019-09-05)

Smart Reply

對於目前的操作會提供更聰明的建議動作。

目前還試不出來(2019-09-05)

Sound Amplifier

能夠幫助過濾雜音,更清楚地聽見周遭的聲音。

基本上就是聲音擴大器 App,要啟動該服務需要到 設定 -> 無障礙設定 -> 聲音擴大器 -> 使用服務。

啟動 聲音擴大器 服務

啟動之後在下方主螢幕按鈕的右側應該會出現人形圖案,可以直接點及該圖案來快速關閉/開啟聲音擴大器。接著插上耳機播放音樂或影片,在播放中點擊人形圖案便會暫停播放音樂或影片,會放大周遭聲音。

嚴格說起來應該叫做周圍聲音放大器,因為該功能對於目前播放的聲音完全沒有影響,啟動之後只會暫停目前播放媒體轉而放大耳機或耳機麥克風聽到的內容。

Gesture Navigation

提供 3 個新的手勢導覽操作,必須到開啟 設定 -> 系統 -> 手勢 -> 系統操作機制 ->透過手勢操作系統。 開啟後原本下方的主螢幕按鈕將不再顯示。

開啟 Gesture Navigation

第 1 個為”前往主螢幕”,手勢為從螢幕底部快速向上滑動。
第 2 個為”切換應用程式”,手勢為從螢幕底部向上滑動之後稍微停住再放開。
第 3 個為”返回上一個畫面”,手勢為從左側或右側往另一側滑動。

實際操作起來,”前往主螢幕”和”切換應用程式”這兩個手勢在流暢度和速度上跟前一個版本比較起來沒有太大差別,且手勢的開始位置都是螢幕底部,所以在操作的便利性上也沒有太大的優勢。

反而”切換應用程式”的新手勢在螢幕底部往上滑動之後還需要稍微停頓一下。而前一個版本不需要停頓反而還比較流暢。

另一個需要討論的是”返回上一個畫面”,”返回上一個畫面”的新手勢容易和操作 Navigation Drawer 的手勢搞混。
兩者差別在於”返回上一個畫面”必須從畫面的最邊邊開始操作,而 Navigation Drawer 大約是離畫面最邊邊 一到兩公分開始操作。

操作手勢在畫面最邊邊開始的缺點在於如果保護套高於手機,操作起來會卡卡的。
優點在於只要在畫面左右邊都可以開始操作,前一個版本就只能點擊畫面左下方開始。

另外對於”返回上一個畫面”可以調整其靈敏度,調太高就更容易和 Navigation Drawer 衝突,使用預設值即可。

Dark theme

據說可以放鬆眼睛以及電力,基本上就是看個人喜好而定。
設定之後對於有些應用程式(Photos, Calendar)也會套用 Dark Theme。


Dark Theme Settings
Dark Theme Calendar

Dark Theme Photos


Privacy Controls

可以更自由的控制自己的隱私。

在設定中多了一個隱私的項目,該項目可以調整關於私有資料的設定。

隱私項目設定

Location Controls

可以更自由的控制自己的位置。

在 設定 -> 定位 選項中,可以查看和設定那些 App 使用了定位功能。

定位 選項

Security Updates

現在可以通過 Google Play 發送安全隱私更新。更快速的取得這些更新,讓裝置處於更安全的狀態。

Focus Mode

在專注模式下可以避免其他應用程式的通知打擾。還可以針對單獨的應用程式設定。

目前還未開放(2019-09-05)

Family Link

管理孩子使用裝置的狀況,如使用裝置時間,安裝那些 App,查看位置。

可以使用,但有個很大的限制為目前被監控方(!!)的裝置限定為 Android 專用。

分類
Android

2019 Google I/O Android App 原始碼釋出!!

GitHub: https://github.com/google/iosched
基本的介紹在GitHub上都有描述,以下為重點總結

開發環境相關

1.整個App都是用kotlin寫的
2.Import的方式可以直接從Android Studio的project from version control,也可以使用git command line 來 fork
3.Android Studio 版本為3.4
 

架構相關

1.這次(2019)的App也是用 Android Architecture Components來開發
關於 Android Architecture Component 可以參考這裡
App的架構遵循Guide to app architecture的內容來建立
2.1 邏輯會從Activity和Fragment移出並放到ViewModel中
2.2 透過LiveData觀察資料並使用Data Binding Library將佈局中的UI元件綁定到應用程序的數據來源
3.1 使用Repository Layer來處理數據的操作。App的數據來源來自不同位置
3.2 User data儲存在Cloud FireStore(遠端或本地端的緩存)
3.3 User preferences和Settings儲存在SharedPreferences
3.4 Conference data存儲在遠端,需要的時候可以儲存在記憶體中以供App使用
4.Repository Modules 負責處理所有的數據
5.實作LightWeight Domain Layer,該層位於data layer和presentation layer之間,用來處理UI線程之外的業務邏輯
參考.\*UseCase.kt files under shared/domain for examples
6.使用Navigation component簡化Activity
7.使用Room搭配Fts4進行全文搜索
8.使用Dagger2進行依賴注入,在很大程度上依靠dagger-android來抽象樣板代碼
9.使用Espresso進行基本測試,使用JUnit和Mockito進行單元測試
 

FireBase 相關

Cloud Firestore
是所有User Data的來源(由使用者加星號或保留的活動)。 Firestore為提供自動同步和無縫管理的離線功能。
Firebase Cloud Functions
允許執行後端代碼。預訂功能在很大程度上取決於與Firestore結合使用的雲功能
Firebase cloud message
可通知app有關服務器上會議數據(Conference data)的更改。
Remote config
幫助管理應用內常量。
.

import & build

目前的環境為Win10,Android Studio 3.4.1
import和build的過程相當順利,沒有出現任何問題需要修改。
直接使用模擬器 run,如下圖

 
接下來就是一步一步分析source code!!

分類
Android

加入 Action View 或 Action Provider 到 App Bar

什麼是 Action View 和 Action Provider?

Action View
是一種Action,在App Bar中提供豐富的功能。如Search Action View可以讓使用者在App Bar中輸入搜尋文字而不必轉換Activity或Fragment。
 
Action Provider
是一種Action,並有客製化介面。一開始顯示為按鈕或menu item。當使用者點擊時,可以用任何方式控制 Action 的行為。
Android support library提供專門的Action View和Action Provider。 例如SearchView用於輸入搜索查詢,ShareActionProvider用於與其他應用程序共享的操作。也可以定義自己的Action View和 Action Provider。
 

加入 Action View

在 app/src /main/res目錄建立 menu 資源文件,並在其中新增<item>元素。<item>元素中加入以下屬性
actionViewClass:實作該action的類別。
actionLayout:佈局資源檔用來描述該action的外觀。
設定showAsAction屬性為“ifRoom | collapseActionView”或“never | collapseActionView”。
collapseActionView用來描述當使用者未互動時如何顯示。如果Action位於App Bar上顯示為icon。如果Action位於overflow menu,則顯示為menu item。當使用者和Action交互時,會填滿App Bar。如下在App Bar中加入 Search View
/app/src/main/res/menu/action_view_actions.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <item android:id="@+id/action_search"
    android:title="search action"
    android:icon="@android:drawable/ic_menu_search"
    app:showAsAction="ifRoom|collapseActionView"
    app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

若使用者未和Action互動時,會顯示為android:icon。若App Bar空間不足則顯示在overflow menu。
如果需要調整Action,可以在Activity的onCreateOptionsMenu方法實作。 可以通過呼叫getActionView方法獲取Action View的參考。 例如,以下為取得SearchView的參考

public class ActionViewActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_action_view);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.action_view_actions, menu);
    MenuItem searchItem = menu.findItem(R.id.action_search);
    SearchView searchView = (SearchView) searchItem.getActionView();
    // Configure the search info and add any event listeners...
    return super.onCreateOptionsMenu(menu);
  }
}

 
啟動App 可以看到App Bar已經加入Search View。
點擊放大鏡icon會出現搜尋框讓使用者輸入內容。

關於如何更進一步的使用SearchView參考這裡https://developer.android.com/guide/topics/search/index.html
完整程式碼如下
/app/src/main/res/layout/activity_action_view.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".action_view_and_action_provider.ActionViewActivity">
  <include
    android:id="@+id/app_toolbar"
    layout = "@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

/app/src/main/res/manu/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

 

加入 Action Provider

在app/src /main/res目錄建立menu 資源文件,並在其中新增<item>元素。<item>元素中加入actionProviderClass屬性並對該屬性設定action provider完整路徑。如
androidx.appcompat.widget.ShareActionProvider
以下新增SharedActionProvider
app/src/main/res/menu/action_provider_actions.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/action_shared"
    android:title="shared action"
    android:icon="@android:drawable/ic_menu_share"
    app:showAsAction="ifRoom"
    app:actionProviderClass="androidx.appcompat.widget.ShareActionProvider" />
</menu>

因為SharedActionProvider具有自己的icon,因此不必提供android:icon屬性。
 

實作 Action Provider 行為

透過在onCreateOptionsMenu方法內初始化ShareActionProvider,並設定其行為。

public class ActionProviderActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_action_provider);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.action_provider_actions, menu);
    initShareActionProvider(menu);
    return super.onCreateOptionsMenu(menu);
  }
  private void initShareActionProvider(Menu menu) {
    MenuItem shareActionMenuItem = menu.findItem(R.id.action_shared);
    ShareActionProvider shareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(shareActionMenuItem);
    Intent sendAction = new Intent(Intent.ACTION_SEND);
    sendAction.setType("text/plain");
    sendAction.putExtra(Intent.EXTRA_TEXT, "share action text");
    shareActionProvider.setShareIntent(sendAction);
  }
}

啟動App可以看到一個分享的icon顯示在App Bar上。

點擊分享icon,會顯示要分享的方式。

完整程式碼如下
/app/src/main/res/layout/activity_action_provider.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".action_view_and_action_provider.ActionProviderActivity">
  <include
    android:id="@+id/app_toolbar"
    layout="@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

/app/src/main/res/menu/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

以上為Action View和Action Provider的範例。
 
以下為App Bar的相關內容
http://34.80.81.192/?p=6149
http://34.80.81.192/?p=6176
http://34.80.81.192/?p=6165

分類
Android

加入 Up Action 到 App Bar

什麼是 Up Action?

Up Action的主要功能是返回其parent activity,使用方式相當簡單只有2個步驟。
1.在AndroidManifest.xml中指定其parent activity
2.在Activity的onCreate取得Action Bar物件並呼叫Action Bar的setDisplayHomeAsUpEnabled(true)來啟動up action。
 

指定 Parent Activity

使用up action必須在AndroidManifest.xml中設定其parent activity是哪個Activity。
這可以透過android:parentActivityName 屬性來設定。
在android 4.1之後使用android:parentActivityName,而android 4.1 之前的版本使用<meta-data>。
如下BasicActionActivity 是 UpActionActivity 的 parent activity
/app/src/main/res/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.codefoxx.toolbarexample">
  <application
...
    <activity
      android:name=".basicaction.UpActionActivity"
      android:parentActivityName=".basicaction.BasicActionActivity">
      <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value="com.codefoxx.toolbarexample.basicaction.BasicActionActivity"
        />
    </activity>
    <activity android:name=".basicaction.BasicActionActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
...

 

啟動 Up Action

可以透過呼叫App Bar的setDisplayHomeAsUpEnabled()方法來啟動Up Action。
一般而言會在Activity建立時呼叫setDisplayHomeAsUpEnabled
如onCreate方法。如下

public class UpActionActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_up_action);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
    ActionBar supportActionBar = getSupportActionBar();
    supportActionBar.setDisplayHomeAsUpEnabled(true);
  }
}

注意
1.不需要在onOptionitemSelected方法內實作up button的動作,因為up button的動作就是返回其parent activity,不需要另外實作。
2.點擊up action時,要返回的parent activity必須已存在於工作棧中,若不存在則會直接關閉目前的Activity。

完整程式碼如下
/app/activity_up_action.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".basicaction.UpActionActivity">
  <include
    android:id="@+id/app_toolbar"
    layout="@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

/app/src/main/res/menu/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

以上為加入Up Action 到 App Bar的範例。
 
以下為App Bar的相關內容。
http://34.80.81.192/?p=6149
http://34.80.81.192/?p=6165
http://34.80.81.192/?p=6192

分類
Android

加入 Action 到 App Bar

什麼是 Action?

Action主要提供在該Activity中最重要的操作, Action會加入到 App Bar 中。如下圖的加號。

例如,照片瀏覽App可能會提供Action作為共享按鈕和創建按鈕,而當查看單張照片時可能會提供Action作為裁剪和過濾按鈕。
但App Bar的空間有限,如果Action數量多於App Bar中可以容納的數量,則App Bar會將多餘的Action放在overflow menu。也可以指定Action應該顯示在overflow menu中而不是顯示在App Bar。
 

建立 Action

所有的Action和其他元件都是定義在menu資源中。
首先在app/src/main/res/menu目錄中建立menu resource file。
並在menu resource file內加入一個<item>元素,該元素定義要顯示的Action如下
/app/src/main/res/menu/main_actions.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/action_add"
    android:title="Add"
    android:icon="@drawable/add"
    android:orderInCategory="1"
    app:showAsAction="ifRoom"
    />
</menu>

android:id=“@+id/action_add”是為了在程式碼中引用Action。android:icon=“@drawable/add”是該Action顯示在App Bar上的圖示android:orderInCategory=“1” 是該Action顯示在App Bar上的順序,越小越靠近左側
app:showAsAction=“ifRoom” 是該Action的圖示如何顯示的設定,可用的選項如下
ifRoom ->app bar有空間再加入,若空間不夠則放入overflow menu
withText -> 加入標題文字
never -> 總是在overflow menu,不在app bar的空間
always -> 總是放在app bar
以上的屬性的說明可以參考這裡
https://developer.android.com/guide/topics/resources/menu-resource
在編輯menu resource file時可以開啟preview視窗,方便查看目前的頁面。

 

加入 Action 到 App Bar

覆寫Activity的onCreatOptionsMenu方法,並在該方法中指定要使用的menu resource file。

package com.codefoxx.toolbarexample.basicaction;
import android.view.Menu;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import com.codefoxx.toolbarexample.R;
public class BasicActionActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_action);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_actions, menu);
    return super.onCreateOptionsMenu(menu);
  }
}

這裡要注意的是若該Activity使用的是Tool Bar,則必須要先設定Tool Bar,也就是上面的

Toolbar toolBar = findViewById(R.id.app_toolbar);
setSupportActionBar(toolBar);

否則下方的onCreateOptionsMenu會失效。
啟動App,便可以看到Action顯示在App Bar中。

實作 Action 行為

接著實作按下該Action時的行為。
覆寫Activity的onOptionsItemSelected方法,該方法就是使用者點擊App Bar上的 Action會觸發的方法。
在該方法中根據在menu resource file的<item> android:id設定的值來判斷按下了哪個Action。
以下範例很簡單的顯示Toast

package com.codefoxx.toolbarexample.basicaction;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import com.codefoxx.toolbarexample.R;
public class BasicActionActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_action);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_actions, menu);
    return super.onCreateOptionsMenu(menu);
  }
  @Override
  public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    int itemId = item.getItemId();
    switch (itemId) {
      case R.id.action_add:
        Toast.makeText(this, "click add action button", Toast.LENGTH_SHORT).show();
        break;
    }
    return super.onOptionsItemSelected(item);
  }
}

完成,以上便是加入Action到App Bar的範例。
完整程式碼如下
/app/src/main/res/layout/activity_basic_action.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".basicaction.BasicActionActivity">
  <include
    android:id="@+id/app_toolbar"
    layout="@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

/app/src/main/res/menu/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

 
以下是App Bar的相關內容。
http://34.80.81.192/?p=6149
http://34.80.81.192/?p=6176
http://34.80.81.192/?p=6192

分類
Android

App Bar 簡介和加入 App Bar 到 Activity

以下內容為參考官網以及實際實作的心得。

App Bar 是什麼?

App Bar (也稱為Action Bar) 是Activity重要的設計元素之一,因為它提供了使用者熟悉的視覺結構和交互元素。使用App Bar可讓App的行為和外觀保持一致,讓使用者能夠快速了解如何操作並獲得良好的體驗。
App Bar的主要功能為
a.為App提供名稱並顯示目前在App中的位置。
b.提供重要操作,例如搜尋。。
c.支持導航和視圖切換。
從Android 3.0(API Level 11)開始,使用預設主題的所有activity都將使用Action Bar作為App Bar。但Action Bar的功能是在各個Android版本中逐漸加入,因此Action Bar的行為可能會有所不同,實際上取決於設備使用的Android版本。
相比之下,在android support library的Tool Bar已加入了最新功能,並且可以在任何可以使用支持函式庫的設備上使用。
因此,應該使用android support library的Tool Bar來實現App Bar,這種方式有助於App在大多數的設備上具有一致的行為。

加入 Tool Bar 到 Activity

1.加入 androidx appcompat library,因為Tool Bar位於該函式庫內
Note:從Android 9開始,Google推薦使用androidx library,因此這裡使用的是androidx,而不是v7 appcompat support library。
詳細內容參考https://developer.android.com/topic/libraries/support-library/setup

Project 的build.gradle加入

buildscript {
    repositories {
        google()
        jcenter()
...
    }
}
allprojects {
    repositories {
        google()
        jcenter()
   ...
    }
}

Module的build.gradle加入

dependencies {
   ...
    implementation 'androidx.appcompat:appcompat:1.0.2'
...
}

 
2.讓Activity繼承 AppCompatActivity

import androidx.appcompat.app.AppCompatActivity;
public class BasicToolBarActivity extends AppCompatActivity {
...
}

 
3. 設定AndroidManifest.xml的 <application> 元素使用appcompat的 NoActionBar 主題
Note:改用NoActionBar的目的就是不使用預設的Action Bar

<application
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    />
...

 
4.加入 Tool Bar 到 Activity 的佈局檔中。
有兩種做法,第一種是直接在Activity的佈局檔中加入(寫死的做法),這種做法的缺點是當要修改Tool Bar時,所有的Activity寫死的部分都要修改。如下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".basictoolbar.BasicToolBarActivity">
  <androidx.appcompat.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
  </androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>

第二種做法是另外建立一個Tool Bar的佈局檔,然後在Activity的佈局檔中使用<include>加入Tool Bar的佈局檔。比較建議這種做法。
如下新增app/src/res/main/layout/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

接著在Activity的佈局檔中使用<include>引入Tool Bar的佈局檔
/app/src/main/res/layout/activity_basic_tool_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".basicaction.BasicActionActivity">
  <include
    android:id="@+id/app_toolbar"
    layout="@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

 
5.在Activity的 onCreate() 中呼叫 setSupportActionBar() 方法並傳入在佈局檔中的Tool Bar物件

package com.codefoxx.toolbarexample.basictoolbar;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import com.codefoxx.toolbarexample.R;
public class BasicToolBarActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_tool_bar);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
}

 
完成,現在就是使用 Tool Bar 作為 App Bar。在預設的情況下會有App的名稱以及overflow menu。
另外使用Tool Bar作為App Bar時就可以呼叫androidx appcompat library的Action Bar類別所提供的方法。
具體的做法是呼叫 getSupportActionBar 方法,該方法返回Action Bar參考,取得Action Bar參考之後便可操縱Tool Bar。如下在程式碼中重新設定App Bar的 title

public class BasicToolBarActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_tool_bar);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
    ActionBar supportActionBar = getSupportActionBar();
    supportActionBar.setTitle("set customize title bar");
  }
}


以上便是加入基本的Tool Bar到Activity的方式。
以下是關於App Bar其他操作。
http://34.80.81.192/?p=6165
http://34.80.81.192/?p=6176
http://34.80.81.192/?p=6192
 

分類
Android

ConstraintLayout

ConstrainLayout 是什麼?

ConstraintLayout可以用來製作大型複雜的布局且沒有巢狀視區群組,類似於RelativeLayout但又比RelativeLayout更有彈性。
ConstraintLayout目前(20190816)已被移至androidx 套件中,套件位置可參考。https://dl.google.com/dl/android/maven2/index.html(穩定版本為1.1.3,beta版本為2.0.0-beta2)
ConstraintLayout可用於API level 9以上可以適用於大多數的裝置。ConstraintLayout操作方式可以透過佈局編輯器的可視化工具來編輯也可以直接修改佈局檔。
 

ConstraintLayout 概述

在ConstraintLayout中的視圖元件(view)最少需要定義一個水平方向和一個垂直方向的約束,每個約束的目標對象可以是另一個視圖,父元件或引導線等等。
如下就是TextView具有水平方向以及垂直方向的約束,水平方向約束為左邊貼齊父元件左邊,垂直方向約束為上邊貼齊父元件上邊。

<TextView
  android:id="@+id/constraint_layout_tv_left_top_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="lt to lt and tp to tp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

官方介紹參考
https://developer.android.com/reference/android/support/constraint/ConstraintLayout#Chains
https://developer.android.com/training/constraint-layout
官方範例
https://github.com/googlesamples/android-ConstraintLayoutExamples
 

ConstraintLayout 相依性

在project的build.gradle加入google repo

buildscript {
    repositories {
        google()
    }
}
allprojects {
    repositories {
        google()
    }
}

在Module的build.gradle加入

dependencies {
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
 }

 

CostraintLayout 的約束類型

Relative Position(相對定位)
Margin(邊距)
Circular positioning(圓狀定位)
Chains(鏈)
Virtual Helper objects(虛擬幫助物件)
另外注意不能在約束中有循環依賴關係。
可以參閱ConstraintLayout.LayoutParams以獲取佈局屬性
 

Relative Position 相對定位

相對定位是根據目標元件設定位置的定位方法,目標元件可以是某個元件或父元件或是虛擬幫助物件(引導線或分界線)。
Relative Position和Relative Layout的用法相當類似,RelativeLayout也是透過android:layout_above等等來指定元件相對於另一個元件的位置。
相對定位的種類可以分為水平和垂直,水平定位有right, left, start, end,垂直定位有top, bottom, baseline。
圖示左邊為垂直定位,右邊為水平定位

可以使用的屬性如下

layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf

可以把屬性解釋為
layout_constraint目前元件邊_to目標元件邊Of=”目標元件”
範例如下

<TextView
  android:id="@+id/constraint_layout_tv_left_top_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="lt to lt and tp to tp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

第6行代表目前元件的左邊貼齊目標元件(父元件)的左邊。
第7行代表目前元件的上邊貼齊目標元件(父元件)的上邊。
圖示如下

目標元件也可以改為某個元件,使用時只要指定目標元件的id即可。
現在新增另一個元件(top to bottom and left to left),該元件的上邊會貼齊目標元件(lt to lt and tp to tp)的下邊,左邊貼齊左邊如下

  <TextView
    android:id="@+id/constraint_layout_tv_left_top_parent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="lt to lt and tp to tp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  <TextView
    android:id="@+id/constraint_layout_tv_top_bottom"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="top to bottom and left to left"
    app:layout_constraintTop_toBottomOf="@+id/constraint_layout_tv_left_top_parent"
    app:layout_constraintLeft_toLeftOf="@+id/constraint_layout_tv_left_top_parent"/>

圖示如下

需要注意的是水平置中為

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"

垂直置中為

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

水平及垂直置中

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

 

Margins 邊距

邊距是用來增加指定邊的距離,需要注意的是想增加的邊距必須是已存在相對的約束才有效果。如下
假設目標元件(center parent)已被設定為垂直和水平置中於父元件

<androidx.constraintlayout.widget.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=".margins.MarginsActivity">
  <TextView
    android:id="@+id/margins_activity_tv_center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="center parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

現在有一元件(margin top 50)下邊貼齊center parent的下邊,左邊貼齊左邊。如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintBottom_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  />

margin top 50元件想增加上邊距50dp,把增加上邊距50dp加入如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintBottom_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  />

這種做法沒有效果,因為目前元件(margin top 50)並沒有對目標元件有Top的約束,只有Bottom和Left的約束。
因此想讓android:layout_marginTop=“50dp”有效果,必須在目標元件內加入constraintTop如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintTop_toTopOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  />


至於是constraintTop_toTopOf還是constraintTop_toBottomOf都可以,只要目標元件具有constraintTop的約束即可。
以下是可以使用的margin

android:layout_marginStart
android:layout_marginEnd
android:layout_marginLeft
android:layout_marginTop
android:layout_marginRight
android:layout_marginBottom

注意margin只能接受大於等於0的數值。
另外當目標元件的可視性為gone時,可以透過layout_goneMarginXXX設定對應的邊距。接續上面的範例,若希望margin top 50在center parent的可見性為gone時,增加上邊距改為100,如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintTop_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  app:layout_goneMarginTop="100dp"
  />

 

Centering positioning and bias 置中定位和偏移

置中定位即為水平置中以及垂直置中。
水平置中為

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"

垂直置中為

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

水平及垂直置中

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

 

bias 偏移

當單純使用水平置中或是垂直置中會讓目標元件置中也就是讓目標元件位於中心位置,但若想讓目標元件不這麼置中,而想偏移一些位置時就可以使用bias。
水平置中偏移為
layout_constraintHorizontal_bias
當水平置中偏移為0.5時就是預設的水平偏移位置。
可以把水平置中偏移當作從左邊移到右邊的位置,數值越靠近0就越靠近左邊,越靠近1就越靠近右邊
垂直置中偏移為
layout_constraintVertical_bias
當垂直置中偏移為0.5時就是預設的垂直偏移位置
可以把垂直置中偏移當作從上邊移到下邊的位置,數值越靠近0就越靠近上邊,越靠近1就越靠近下邊
範例如下
假設目前元件已經被設定為垂直置中了

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center parent"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintTop_toTopOf="parent"
  />

現在想讓目標元件靠近上邊則可如下設定

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center to ver"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintTop_toTopOf="parent"
  app:layout_constraintVertical_bias="0.1"
  />


 
水平偏移也是相同的做法。但需要注意以下的限制。
偏移必須有相對的約束才有效。
垂直偏移必須在constraintBottom 以及 constraintTop都存在的情況才有效。
水平偏移必須在constraintLeft以及 constraintRight都存在的情況才有效。

Circular positioning (1.1) 圓狀定位

可以透過角度和半徑來約束目標元件和目前元件的位置。使用方法如下
layout_constraintCircle : references another widget id
layout_constraintCircleRadius : the distance to the other widget center
layout_constraintCircleAngle : which angle the widget should be at (in degrees, from 0 to 360)
基本上就是決定目標元件,半徑,角度。
範例如下

<TextView
  android:id="@+id/center_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center parent"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintRight_toRightOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>
<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="current"
  app:layout_constraintCircle="@id/center_parent"
  app:layout_constraintCircleAngle="360"
  app:layout_constraintCircleRadius="100dp"/>

current元件會把center parent當作中心點,移動到角度為360度,半徑為100dp的位置

 

Dimensions constraints 尺寸約束

可以為ConstraintLayout定義最大和最小的寬度和高度。如下
android:minWidth   -> set the minimum width for the layout
android:minHeight  -> set the minimum height for the layout
android:maxWidth  -> set the maximum width for the layout
android:maxHeight  -> set the maximum height for the layout
 
Widgets dimension constraints 元件尺寸約束
元件的尺寸(width and height)可以透過3種方式來設定
a. 使用1個指定的尺寸
b. 使用wrap_content以符合內容大小
c. 使用0dp等同於MATCH_CONSTRAINT,會受margin影響
c. 用法比較特別需要另外注意。
當width = 0dp,constraintLeft_toLeftOf = “parent” 且 constraintRight_toRightOf =”parent”,代表元件寬度會填滿父元件的寬度。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".dimensionsconstraint.DimensionConstraintActivity">
  <Button
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="top"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


0dp會受margin影響,增加左右兩邊margin 50dp如下

<Button
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:text="top"
  android:layout_marginLeft="50dp"
  android:layout_marginRight="50dp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintRight_toRightOf="parent"
  />


 
若約束參考到另一個元件,其width & height也會受影響,如下

<Button
  android:id="@+id/dimension_constrraint_btn_left_top"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="left top"/>
<Button
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:text="top center"
  app:layout_constraintLeft_toRightOf="@+id/dimension_constrraint_btn_left_top"
  app:layout_constraintRight_toLeftOf="@+id/dimension_constrraint_btn_right_top"/>
<Button
  android:id="@+id/dimension_constrraint_btn_right_top"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="right top"
  app:layout_constraintTop_toTopOf="parent"
  app:layout_constraintRight_toRightOf="parent"/>

top center雖然width是0dp,但其左右約束都參考到其他元件,因此不會填滿父元件。

建議在ConstraintLayout的元件不要使用MATCH_PARENT,相同的效果可以透過MATCH_CONSTRAINT加上left, right, top, bottom來達到
 
WRAP_CONTENT:強制約束(1.1
如果尺寸設置為WRAP_CONTENT,則在1.1之前的版本中約束不會限制尺寸。
在某些情況下希望使用WRAP_CONTENT且強制執行約束以限制結果。可以透過以下的屬性達成:
app:layout_constrainedWidth=”true|false”
app:layout_constrainedHeight=”true|false”
 
MATCH_CONSTRAINT尺寸(1.1
當尺寸設置為MATCH_CONSTRAINT時,預設會占用所有空間。 還有幾個額外的屬性可以使用:
layout_constraintWidth_min和layout_constraintHeight_min:將設置尺寸的最小大小
layout_constraintWidth_max和layout_constraintHeight_max:將設置尺寸的最大大小layout_constraintWidth_percent和layout_constraintHeight_percent:將設置尺寸的大小設置為父元件的百分比
Min and Max
Min和max的單位可以設定為xxdp,或是wrap代表wrap_content。

Percent dimension 百分比尺寸

Percent dimension類似於LinearLayout中的設定weight的設定,使用百分比尺寸需要設定以下的內容。
1.尺寸(width or height)必須設定為0dp(match_constraint)
2.app:layout_constraintWidth_default=”percent” 和app:layout_constraintHeight_default=”percent”的預設值應該設定為percent
3.layout_constraintWidth_percent or layout_constraintHeight_percent 為0到1之間的數值
範例如下,設定3個TextView寬度各佔其父元件的0.33333

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <TextView
    android:id="@+id/top_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="top"
    android:textSize="100sp"
    android:background="@color/colorPrimary"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  <TextView
    android:id="@+id/down_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="middle"
    android:textSize="100sp"
    android:background="@color/colorAccent"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/top_button"
    />
  <TextView
    android:id="@+id/middle_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="bottom"
    android:textSize="100sp"
    android:background="@color/colorPrimaryDark"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/down_button"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

圖示如下

 

Ratio(寬高比)

可以設定元件的寬高比,首先設定其中一個方向(寬或高)為0dp,再透過layout_constraintDimensionRatio 設定比例,如下

  <Button
    android:layout_width="0dp"
    android:layout_height="100dp"
    android:text="bottom button"
    app:layout_constraintDimensionRatio="2:1"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    />

因為ratio為2:1,而width為0dp,代表width會隨著height變化,而height為100dp,因此width為200dp。
若將width或height都設定為0dp,系統會嘗試滿足最大的比例如下

  <Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="top button"
    app:layout_constraintDimensionRatio="2:1"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

設定ratio為2:1,width和height都為0dp。此時系統會讓width為填滿父元件,而height為width的一半。
圖示如下

chains 

鏈提供了對單一方向的元件群組。另一個方向可以獨立變化。
若是一個元件群組被雙向(反方向)連結在一起,就可視為1個鏈,如下圖。

 
Chain heads 鏈首
鏈首為鏈的第一個元素,水平方向的鏈首為最左邊的元件,垂直方向的鏈首為最上方的元件。

Margins in chains 鏈中的邊距

如果鏈中指定了邊距,會套用在鏈中的每個元件之間。使用邊距時,其效果是相加的。
若一個水平鏈有一個元素設定右邊距為10dp,而下一個元素的左邊距為5dp則這兩個元素的邊距為15dp
 
Chains Style 鏈的類型

可以在鏈首元素設定layout_constraintHorizontal_chainStyle or layout_constraintVertical_chainStyle以指定鏈的類型,預設是CHAIN_SPREAD。
以下是鏈的類型
CHAIN_SPREAD
鏈中的元素將會展開(預設),範例如下,2個按鈕的水平鏈

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 
Weighted chain
在CHAIN_SPREAD中,如果有元素設定為MATCH_CONSTRAINT(0dp)將會使用剩下的空間。
範例如下,2個按鈕的水平鏈,first按鈕的width為0dp,因此會占用剩下來的空間。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_SPREAD_INSIDE
在鏈的2端不會有空隙。
範例如下,2個按鈕,設定為chain style設定為spread_inside

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_PACKED
鏈內的元件將會被連結在一起,不會有空隙。
範例如下,2個按鈕,chain style設定為 packed

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="packed"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_PACKED_WITH_BIAS
鏈內的元件會被連結在一起且會透過bias屬性設定比例。
範例如下,2個按鈕,chain style設定為package,且鏈首設定 Horizontal_bias 為 0.2

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_bias="0.2"
    app:layout_constraintHorizontal_chainStyle="packed"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 

 
Weighted chains 權重鏈
鏈中的元素若使用MATCH_CONSTRAINT(0dp),預設會占用所有剩餘的可用空間。
可以加上layout_constraintHorizontal_weight 或 layout_constraintVertical_weight屬性來指定占用空間的比例。
範例如下,2個按鈕,個別設定layout_constraintHorizontal_weight為0.7,0.3

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="0.7"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    app:layout_constraintHorizontal_weight="0.3"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 
另外一個重要使用方式為占用的總空間可以指定為其他元件,不一定只能是父元件,可以透過改變鏈首起始邊的參考元件以及鏈尾結束邊的參考元件。
如下新增2個Guideline,位置各在水平方向的0.2(guideline_0.2)以及0.8(guideline_0.8)
修改first按鈕的起始邊參考到guideline_0.2,以及second按鈕的結束邊參考到guideline_0.8

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".Chains.ChainsActivity">
  <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline_0.2"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.2"/>
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="@+id/guideline_0.2"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="0.3"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="@+id/guideline_0.8"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    app:layout_constraintHorizontal_weight="0.7"
    />
  <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline_0.8"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.8"/>
</androidx.constraintlayout.widget.ConstraintLayout>


 

Virtual Helper objects 虛擬幫助物件

在ConstraintLayout可以使用Guideline(引導線)和Barrier(分界線)來幫助元件定位。
Guideline(引導線)
Guideline 主要是用於幫助其他元件定位,可以先在佈局檔中建立Guideline,讓其他元件根據Guideline設定位置,Guideline不會顯示在介面上,Guideline可以是水平或垂直方向。以下是一個最簡單的Guideline

<androidx.constraintlayout.widget.Guideline
  android:id="@+id/middle_guide_line"
  android:layout_width="0dp"
  android:layout_height="0dp"
  android:orientation="horizontal"
  app:layout_constraintGuide_percent="0.5"/>

有幾個屬性需要注意, Guideline會有id以讓其他元件定位,width和height通常設為0dp,orientation必須設定為vertical或horizontal以指定水平或垂直方向。
重點為Guide_percent,該值代表Guideline位於畫面上的哪個比例位置,數值為從0到1的小數點。
若是水平方向的Guideline,0為最上方,1為最下方,垂直方向的Guideline 0為最左方,1為最右方,如上例是一個percent為0.5且水平方向的Guideline代表位於畫面中心的橫線。

新增按鈕並讓其上邊貼齊Guideline的上邊

<Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="top middle guide line"
  app:layout_constraintTop_toTopOf="@id/middle_guide_line"
  />


比較複雜的畫面通常需要先對畫面進行規劃,就適合先建立Guideline再讓其他的元件根據Guideline進行定位。
Barrier(分界線)
Barrier和Guideline類似不會顯示,也是用來幫助其他元件定位用,主要的差別在於Barrier通常是根據多個目標元件定位,定位之後會根據目標元件的變化動態改變自己的位置。
範例如下,在畫面左側先加入Account和Password 2個EditText,再增加Barrier,Barrier會定位在Account和Password的右邊,最後加入TextView會定位在Barrier的右邊。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".barrier.BarrierActivity">
  <EditText
    android:id="@+id/account"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="Account : "
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toTopOf="parent"
    />
  <EditText
    android:id="@+id/password"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="Password : "
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toBottomOf="@+id/account"
    app:layout_constraintLeft_toLeftOf="parent"
    />
  <androidx.constraintlayout.widget.Barrier
    android:id="@+id/left_barrier"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="horizontal"
    app:barrierDirection="right"
    app:constraint_referenced_ids="account, password"
    />
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="position to barrier"
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toTopOf="@+id/account"
    app:layout_constraintLeft_toRightOf="@+id/left_barrier"/>
</androidx.constraintlayout.widget.ConstraintLayout>

圖示如下

因為Barrier根據Account和Password定位,只要這2個元件有變化,則Barrier就會跟著變化。
在Account輸入一連串內容以改變Barrier位置,Barrier位置改變之後position to barrier也會改變。

 
 

分類
Android

綁定型服務

綁定型服務是什麼?

綁定型服務是服務的一種,可以綁定應用程式元件並透過已定義的介面和元件互動。元件可以透過該介面呼叫服務的方法。
 

實作綁定型服務

透過繼承Service類別來建立綁定型服務,需要實作onBind方法,就是最簡單的綁定型服務。

public class BindService extends Service {
  public BindService() {
  }
  @Override
  public IBinder onBind(Intent intent) {
    // TODO: Return the communication channel to the service.
    throw new UnsupportedOperationException("Not yet implemented");
  }
}

重點為onBind方法,該方法會在元件綁定服務時執行,參數intent會從元件傳遞進來以攜帶需要從元件傳入的資料。
onBinder方法會回傳IBinder,當IBinder回傳到元件之後,元件便可透過IBinder和服務互動。
 

實作IBinder

IBinder是一個介面,其用途為讓元件取得服務的參考以進行和服務之間的互動。
因為Android已提供Binder類別,Binder類別已經實作IBinder介面中重要的方法。所以我們不必自己再實作IBinder介面,只需要建立Binder的子類別,並在該子類別中傳回服務的參考即可。

public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

ServiceBinder類別的getBindService方法會回傳服務本身,元件就是透過這個方法取得服務的參考並與其互動。

實作ServiceConnection

當元件和服務進行綁定時是透過呼叫bindService方法來執行,如下
bindService(IntentToService, serviceConnect, Context.BIND_AUTO_CREATE);
該方法第2個參數為ServiceConnect物件,元件透過ServiceConnect物件取得目前和服務綁定的狀況(已連結或已中斷),我們需要建立ServiceConnect物件並實作2個方法,分別為

@Override
public void onServiceConnected(ComponentName name, IBinder service) {}

onServiceConnected方法會在元件綁定服務時被呼叫,該方法的IBinder參數即為從服務的onBind方法回傳,我們就可以透過該參數來取得服務。

@Override
public void onServiceDisconnected(ComponentName name) {}

onServiceDisconnected方法會在元件和服務中斷時被呼叫。
建立BindServiceActivity如下

public class BindServiceActivity extends AppCompatActivity {
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
  }
}

接著在ServiceConnection的onServiceConnected方法中實作透過傳入的binder取得服務的參考。

private BindService mBindService;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
  }
};

另外在元件中還需要一個布林變數來記錄目前是否已綁定服務,這個變數主要是方便元件對服務進行綁定或解綁定。

private boolean mIsServiceBound;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
    mIsServiceBound = true;
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
    mIsServiceBound = false;
  }
};

 

綁定服務或解綁定服務

以Activity來說綁定服務的實作位置通常會在onCreate或onStart,而解綁定的位置會對應綁定的位置。
若Activity移到後台時也要持續從服務收到更新,那就在onCreate綁定然後在onDestroy解綁定。
若Activity只有在前台顯示時需要收到服務的更新,在後台時不需要。就在onStart綁定然後在onStop解綁定。
以下在onCreate綁定服務,在onDestroy解綁定服務。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_bind_service);
  Intent bindService = new Intent(this, BindService.class);
  if (!mIsServiceBound) {
    bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
  }
}
@Override
protected void onDestroy() {
  super.onDestroy();
  if (mIsServiceBound) {
    unbindService(mServiceConnection);
    mIsServiceBound = false;
  }
}

最後在Activity加入按鈕,按鈕按下時就會從服務取得亂數

@Override
public void onClick(View v) {
  int uiID = v.getId();
  switch (uiID) {
    case R.id.bind_service_get_service_random_number:
      if (mIsServiceBound) {
        Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
      }
      break;
  }
}

 
以下為完整程式碼

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumber;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}
public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

 

綁定型服務的生命週期

在BindService複寫onCreate, onBind, onStartCommand, onUnbind, onDestroy加入log來觀察生命週期。

public class BindService extends Service {
…
  public BindService() {
    Log.d(TAG, "BindService: ");
  }
  @Override
  public void onCreate() {
    Log.d(TAG, "onCreate: ");
    super.onCreate();
  }
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  public IBinder onBind(Intent intent) {
    Log.d(TAG, "onBind: ");
    return mBinder;
  }
  @Override
  public boolean onUnbind(Intent intent) {
    Log.d(TAG, "onUnbind: ");
    return super.onUnbind(intent);
  }
  @Override
  public void onDestroy() {
    Log.d(TAG, "onDestroy: ");
    super.onDestroy();
  }
…
}

在BindServiceActivity複寫onCreate, ServiceConnection的onServiceConnected, onServiceDisconnected加入log觀察生命週期

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      Log.d(TAG, "Activity call bindService: ");
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      Log.d(TAG, "Activity call unbindService: ");
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

啟動Activity後的log為

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

需要注意的是onServiceConnected方法是在服務呼叫onBind方法之後呼叫
點擊back key之後的log為

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

需要注意的是當元件透過呼叫unbindService去解綁定服務時, ServiceConnection的onServiceDisconnected方法不會被呼叫。
 

多個元件綁定和解綁定服務

綁定型服務可以和多個元件一起綁定。和多個元件綁定之後只有當所有的元件都解綁定該服務才會銷毀。
為了測試多個元件綁定服務,新增AnotherBindServiceActivity,如下

public class AnotherBindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = AnotherBindServiceActivity.class.getSimpleName();
  private Button mBindServiceBtn;
  private Button mUnbindServiceBtn;
  private Button mGetRandomNumberBtn;
  private Button mLaunchBindServiceActivityBtn;
  private boolean mIsServiceBound;
  private BindService mBindService;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_another_bind_service);
    mLaunchBindServiceActivityBtn = findViewById(R.id.another_bind_service_start_bind_service_activity);
    mLaunchBindServiceActivityBtn.setOnClickListener(this);
    mBindServiceBtn = findViewById(R.id.another_bind_service_bind_service);
    mBindServiceBtn.setOnClickListener(this);
    mUnbindServiceBtn = findViewById(R.id.another_bind_service_unbind_service);
    mUnbindServiceBtn.setOnClickListener(this);
    mGetRandomNumberBtn = findViewById(R.id.another_bind_service_get_service_random_number);
    mGetRandomNumberBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "AnotherBindServiceActivity is:"+this.toString());
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch(uiID){
      case R.id.another_bind_service_start_bind_service_activity:
        Intent launchBindServiceActivity = new Intent(this, BindServiceActivity.class);
        launchBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchBindServiceActivity);
        break;
      case R.id.another_bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.another_bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.another_bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

重點在於AnotherBindServiceActivity也可綁定BindService,目前有BindServiceActivity和AnotherBindServiceActivity可綁定BindService。
另外修改BindServiceActivity新增按鍵用來綁定和解綁定以及啟動AnotherBindServiceActivity,如下

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumberBtn;
  private Button mtBindServiceBtn;
  private Button mtUnBindServiceBtn;
  private Button mStartAnotherBindServiceActivityBtn;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceConnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumberBtn = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumberBtn.setOnClickListener(this);
    mtBindServiceBtn = findViewById(R.id.bind_service_bind_service);
    mtBindServiceBtn.setOnClickListener(this);
    mtUnBindServiceBtn = findViewById(R.id.bind_service_unbind_service);
    mtUnBindServiceBtn.setOnClickListener(this);
    mStartAnotherBindServiceActivityBtn = findViewById(R.id.bind_service_start_another_bind_service_activity);
    mStartAnotherBindServiceActivityBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "BindServiceActivity is:"+this.toString());
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
      case R.id.bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.bind_service_start_another_bind_service_activity:
        Intent launchAnotherBindServiceActivity = new Intent(this, AnotherBindServiceActivity.class);
        launchAnotherBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchAnotherBindServiceActivity);
        break;
    }
  }
}

注意BindServiceActivity和AnotherBindServiceActivity是透過FLAG_ACTIVITY_REORDER_TOFRONT來互相切換,並不是產生新的Activity實體。
現在先啟動BindServiceActivity並點擊bind service按鈕,Log如下

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

可以看出服務已被綁定,接著點擊按鈕啟動AnotherBindServiceActivity並在該Activity中點擊bind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call bindService:
D/AnotherBindServiceActivity: onServiceConnected:

AnotherBindServiceActivity綁定服務之後服務不會再呼叫建構式以及onCreate, onBind方法,因為該服務已啟動。
接著點擊AnotherBindServiceActivity的unbind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call unbindService:

可以看到該服務並未銷毀,只是和AnotherBindServiceActivity解綁定。
接著按下start BindServiceActivity按鈕來啟動BindServiceActivity,啟動之後點擊unbind service按鈕,Log如下。

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

這時候因為所有和該服務綁定的元件都解綁定服務,服務銷毀。
 

綁定型服務的優缺點

綁定型服務的優點在於可以和多個元件互動,傳遞訊息。
缺點在於服務本身的執行也是在UI Thread中,沒有另外開啟新的執行緒來執行