Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android最佳實踐之UI

Android最佳實踐之UI

編輯:關於Android編程

為多屏設計(一) - 支持多個屏幕尺寸

參考地址:http://developer.android.com/training/multiscreen/index.html
Android UI設計提供了一個靈活的框架,允許應用程序為不同設備顯示不同的布局,創建自定義UI部件,在App外部控制系統的Window。
Android的設備尺寸參差不齊,從幾寸的小手機到幾十寸的TV設備,我們需要學會為這麼多的設備做出適配讓盡可能多的人有更好的體驗。支持多個屏幕尺寸有以下幾種方式:
- 確保你的布局可以充分調整大小以適應屏幕
- 根據屏幕配置提供適當的UI布局
- 確保正確的布局應用到正確的屏幕
- 提供可縮放的Bitmap

使用”wrap_content” 和”match_parent”

一句話:少用固定的dp來設置寬高,不利於屏幕適配。


    
        
        
        

layout-hvga

使用相對布局(RelativeLayout)

雖然你可以使用LinearLayout和”wrap_content” 、”match_parent”來創建一個相當復雜的布局,但不能精確地控制子View之間以及子View和父View之間的關系。比如屏幕方向變化時,為保證子View能隨著變化而保持對父View的相對位置不變,這時,就必須使用RelativeLayout了。



    
    

relativelayout1
relativelayout2
以上兩張圖顯示,橫豎屏切換時,Cancel和OK按鈕的相對位置以及相對屏幕的位置都沒變。

使用Size限定符

上面兩種布局雖然可以適配一定的屏幕,但無法適配一些特定的屏幕尺寸。
比如,對於“列表”和“詳細”,一些屏幕實現“兩屏”的模式,特別是在平板和TV上,但在手機上,必須將兩屏分開在兩個界面顯示。

res/layout/main.xml


    
res/layout-large/main.xml

    
    

注意:large限定符,它修飾的布局,在大屏幕上顯示(如在7寸及以上尺寸的平板上),而沒有任何限定符的,則在一些較小的設備上顯示。

使用最小寬度限定符( Smallest-width Qualifier)

很多App希望不同的大屏上顯示不同的布局(比如在5寸和7寸的大屏上),這就是為什麼在Android 3.2上出現最小寬度限定符的原因。
Smallest-width限定符允許你使用一個確定的最小的dp單位的寬度應用到目標設備上。如果你想在大屏上使用左右窗格顯示,可以像上面那種模式一樣,寫多個相同的布局,這次不用large限定符了,用sw600dp,表示在寬度600dp及以上的設備上將使用我們定義的布局:

res/layout/main.xml


    
res/layout-sw600dp/main.xml

    
    

這意味著寬度大於等於600dp的設備將使用layout-sw600dp/main.xml(雙窗格模式)的布局,小於這個寬度的設備使用layout/main.xml(單窗格模式)。
然而,在Android3.2以前的設備,是不能識別sw600dp這種限定符的,所以為了兼容你必須使用large限定符。再增加res/layout-large/main.xml,讓裡面的內容和res/layout-sw600dp/main.xml一模一樣。在下一部分,你將看到一種技術,它允許你避免重復定義這樣的布局文件。

使用布局別名(Layout Alias)

在使用smallest-width限定符時,由於它是3.2才有的,所以在兼容以前老版本時,需要再重復定義large限定符的布局文件,這樣會對以後的開發維護帶來麻煩。為了避免這種情況,我們使用布局別名,比如:

res/layout/main.xml, 單窗格

res/layout/main_twopanes.xml,多窗格
然後加下面兩個文件:

res/values-large/layout.xml:


    @layout/main_twopanes
res/values-sw600dp/layout.xml:

    @layout/main_twopanes

這樣解決了維護多個相同布局文件的麻煩。

使用方向限定符

一些布局可以在橫屏和豎屏自動調整的很好,但大部分還是需要手工調整的。比如在NewsReader例子中,不同的屏幕尺寸不同的方向,顯示的Bar是不一樣的:

small screen, portrait: single pane, with logo small screen, landscape: single pane, with logo 7” tablet, portrait: single pane, with action bar 7” tablet, landscape: dual pane, wide, with action bar 10” tablet, portrait: dual pane, narrow, with action bar 10” tablet, landscape: dual pane, wide, with action bar TV, landscape: dual pane, wide, with action bar
所以每一個布局都定義在res/layout/目錄下,每一個布局都對應一個屏幕配置(大小和方向)。app使用布局別名來匹配它們到各自對應的設備:
res/layout/onepane.xml:


    

res/layout/onepane_with_bar.xml:


    
        
        
        

res/layout/twopanes.xml:


    
    

res/layout/twopanes_narrow.xml:


    
    

既然所有的布局都定義好了,現在就只需要將正確的布局對應到每個配置的文件中。如下使用布局別名技術:
res/values/layouts.xml:


    @layout/onepane_with_bar
    false

res/values-sw600dp-land/layouts.xml:


    @layout/twopanes
    true

res/values-sw600dp-port/layouts.xml:


    @layout/onepane
    false

res/values-large-land/layouts.xml:


    @layout/twopanes
    true

res/values-large-port/layouts.xml:


    @layout/twopanes_narrow
    true

使用Nine-patch圖片

要支持不同的屏幕分辨率,那麼圖片也需要做成多個尺寸的,這樣勢必增加美工的工作量。如果應用圖片較多,那麼可想而知這時一件多麼可怕的事情。這時,9-patch圖片可以很好的解決這個問題,它可以隨著屏幕的變化而伸展而不變形。
9-patch圖片是.9.png為後綴的圖片,它使用SDK目錄下tools文件夾下的draw9patch.bat的工具來制作。

樣例代碼:NewsReader

為多屏設計(二) - 支持不同的屏幕密度

參考地址:http://developer.android.com/training/multiscreen/screendensities.html
本文向你展示如何通過提供不同的資源和使用分辨率無關的測量單位支持不同的屏幕密度。

使用像素密度

一句話,使用dp(尺寸、距離等)、sp(文本)單位,盡量不用px單位。
例如:

指定文本大小,使用sp:

提供可選擇的Bitmap

因為Android運行在各個屏幕密度不同的設備中,所以你需要為不同的密度設備提供不同的圖片資源 low, medium, high and xhigh 等等。

xhdpi: 2.0 hdpi: 1.5 mdpi: 1.0 (baseline) ldpi: 0.75
意思是說,如果你為一個密度是2 的設備准備了200x200的圖片,那麼同時需要為密度為1.5的設備准備150x150的圖片,為密度為1的設備准備100x100的圖片,為密度為0.75的設備准備75x75的圖片。
然後在res目錄下生成多個drawable文件夾:
MyProject/
  res/
    drawable-xhdpi/
        awesomeimage.png
    drawable-hdpi/
        awesomeimage.png
    drawable-mdpi/
        awesomeimage.png
    drawable-ldpi/
        awesomeimage.png

你通過引用@drawable/awesomeimage,系統將通過屏幕的dpi找到合適的圖片。

將app啟動logo放在mipmap/文件夾下:

res/...
    mipmap-ldpi/...
        finished_launcher_asset.png
    mipmap-mdpi/...
        finished_launcher_asset.png
    mipmap-hdpi/...
        finished_launcher_asset.png
    mipmap-xhdpi/...
        finished_launcher_asset.png
    mipmap-xxhdpi/...
        finished_launcher_asset.png
    mipmap-xxxhdpi/...
        finished_launcher_asset.png

你應該將app啟動圖片放在res/mipmap-[density]/文件夾,而不是drawable/下面,以確保使用最佳的分辨率

為多屏設計(三) - 實現適配的UI流

參考地址:http://developer.android.com/training/multiscreen/adaptui.html
根據應用程序目前顯示的布局,界面流可能會有所不同。例如,如果你的App是雙窗格模式,點擊左側窗格上的一個item,將在右邊的面板中顯示對應的內容;如果是在單窗格模式下,顯示的內容應該在一個新的Activity裡。

確定當前的布局

判斷當前布局是單窗格模式還是多窗格模式(如在NewsReader App中):

public class NewsReaderActivity extends FragmentActivity {
    boolean mIsDualPane;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        View articleView = findViewById(R.id.article);
        mIsDualPane = articleView != null && 
                        articleView.getVisibility() == View.VISIBLE;
    }
}

在使用一個組件之前需要檢測是否為null。比如,在NewsReader的例子App中,有一個按鈕只在Android 3.0 一下的版本下運行時才出現,3.0以上的版本顯示Actionbar(API11+),所以在操作這個按鈕時,應該這樣做:

Button catButton = (Button) findViewById(R.id.categorybutton);
OnClickListener listener = /* create your listener here */;
if (catButton != null) {
    catButton.setOnClickListener(listener);
}

根據當前的布局React

當前布局不同,那麼點擊同樣的item,會產生不同的效果。比如,在NewsReader中,單窗格模式,點擊item,會進入一個新的Activity,雙窗格模式下,點擊item(左側),右側則顯示相應的內容:

@Override
public void onHeadlineSelected(int index) {
    mArtIndex = index;
    if (mIsDualPane) {
        /* display article on the right pane */
        mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
    } else {
        /* start a separate activity */
        Intent intent = new Intent(this, ArticleActivity.class);
        intent.putExtra("catIndex", mCatIndex);
        intent.putExtra("artIndex", index);
        startActivity(intent);
    }
}

同樣,在雙窗格模式下,你應該在ActionBar上創建Tabs導航,反而言之,如果app是單窗格模式,你應該使用Spinner組件進行導航。所以代碼如下:

final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };

public void onCreate(Bundle savedInstanceState) {
    ....
    if (mIsDualPane) {
        /* use tabs for navigation */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);
        int i;
        for (i = 0; i < CATEGORIES.length; i++) {
            actionBar.addTab(actionBar.newTab().setText(
                CATEGORIES[i]).setTabListener(handler));
        }
        actionBar.setSelectedNavigationItem(selTab);
    }
    else {
        /* use list navigation (spinner) */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);
        SpinnerAdapter adap = new ArrayAdapter(this, 
                R.layout.headline_item, CATEGORIES);
        actionBar.setListNavigationCallbacks(adap, handler);
    }
}

在其他Activities重用Fragments

復用模式在多屏狀態下比較常用。比如在NewsReader App中,News詳情采用一個Fragment,那麼它既可以用在雙窗格模式的右邊,又可以用在單窗格模式下的詳情Activity中。
ArticleFragment在雙窗格模式下:


    
    

在單窗格模式下,不需要為ArticleActivity創建布局,直接使用ArticleFragment:

ArticleFragment frag = new ArticleFragment();
getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();

要深深記住的一點是,不要讓Fragment和你的Activity有強的耦合性。你可以在Fragment中定義接口,由Activity來實現。比如News Reader app的HeadlinesFragment :

public class HeadlinesFragment extends ListFragment {
    ...
    OnHeadlineSelectedListener mHeadlineSelectedListener = null;

    /* Must be implemented by host activity */
    public interface OnHeadlineSelectedListener {
        public void onHeadlineSelected(int index);
    }
    ...

    public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
        mHeadlineSelectedListener = listener;
    }
}

當用戶選擇了一個標題,fragment就會通知綁定到指定Activity的監聽器:

public class HeadlinesFragment extends ListFragment {
    ...
    @Override
    public void onItemClick(AdapterView parent, 
                            View view, int position, long id) {
        if (null != mHeadlineSelectedListener) {
            mHeadlineSelectedListener.onHeadlineSelected(position);
        }
    }
    ...
}

處理屏幕配置更改(Configuration Changes)

如果你使用單獨的Activity實現界面的一些部分,那麼就應該記住當屏幕變化時(比如旋轉屏幕)需要重構界面。
比如,在7寸運行Android3.0及以上版本的平板上,NewsReader App在豎屏時新聞詳情界面在單獨的一個Activity中,但是在橫屏時,它使用雙窗格模式,左右各一個Fragment。
這意味著,當用戶豎屏切換到橫屏來看一個新聞詳情,你需要監測到屏幕的變化,來進行適當的重構:結束當前的Activity,顯示左右兩個窗格。

public class ArticleActivity extends FragmentActivity {
    int mCatIndex, mArtIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCatIndex = getIntent().getExtras().getInt("catIndex", 0);
        mArtIndex = getIntent().getExtras().getInt("artIndex", 0);

        // If should be in two-pane mode, finish to return to main activity
        if (getResources().getBoolean(R.bool.has_two_panes)) {
            finish();
            return;
        }
        ...
}

示例代碼:NewsReader

AppBar(ActionBar->ToolBar)

參考地址:http://developer.android.com/training/appbar/index.html

創建應用程序欄(AppBar)

在大多數的App中都會有標題欄這個組件,這個組件能讓App有統一的風格,它一般由標題和溢出菜單(overflow menu)組成。
appbar_basic
從Android3.0開始,使用默認主題的Activity都有一個ActionBar作為標題欄。然而,隨著版本的不斷升級,不斷有新的特性加到ActionBar中,導致不同的版本ActionBar的行為不太一樣。相比之下,支持庫(support library)中的ToolBar不斷集成了最新的特性,而且可以無差異的運行到任意的設備上。
基於此,我們使用支持庫中的Toolbar作為AppBar,他提供各種設備一致的行為。

在Activity中添加Toolbar

添加v7 appcompat support library到工程中 確保Activity繼承AppCompatActivity 在manifest文件的 元素中使用 NoActionBar主題

4.在Activity的布局中添加Toolbar

Material Design specification推薦App Bar有一個4dp的elevation。

5.在Activity的onCreate()方法中,調用Activity的setSupportActionBar(),使ToolBar作為Activity的App bar。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);
    Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
    setSupportActionBar(myToolbar);
    }

這樣你就創建了一個基本的actionbar,上面默認是app的名稱和一個溢出菜單。菜單中只有Settings選項。

使用 App Bar的實用方法

在Activity的標題欄創建了toolbar 之後,你可以使用v7 appcompat support library提供的ActionBar類的很多實用方法,比如顯示和隱藏App bar。
通過getSupportActionBar()得到兼容的ActionBar,如果要隱藏它,可以調用ActionBar.hide()。

添加和處理Actions

Appbar是有限的,所以在它上面添加action,就會溢出(overflow),可以選擇將其放到menu中。
appbar_with_button
圖:添加了一個按鈕的AppBar

添加Action 按鈕

在menu資源中添加item來添加菜單選項:



	  

app:showAsAction屬性表示哪個action可以顯示到appbar上。如果設置為app:showAsAction=”ifRoom”,則appbar上有空間顯示在appbar上,沒有空間就藏在菜單中;如果設置app:showAsAction=”never”,則這個action永遠都在菜單中,不會顯示在appbar上。

響應Actions

在Activity中的onOptionsItemSelected()中處理菜單的點擊事件:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.action_settings:
            // User chose the "Settings" item, show the app settings UI...
            return true;

        case R.id.action_favorite:
            // User chose the "Favorite" action, mark the current item
            // as a favorite...
            return true;

        default:
            // If we got here, the user's action was not recognized.
            // Invoke the superclass to handle it.
            return super.onOptionsItemSelected(item);

    }
}

添加一個Up Action

為了用戶方便的返回主界面,我們需要在Appbar上提供一個Up 按鈕。當用戶點擊Up按鈕,app則返回到父Activity。

聲明父Activity

例如:


    ...

    

    
        ...
    

    
    

        
        
    

讓Up按鈕可用(Enable)

要讓Up按鈕可用,需要在onCreate()方法中調用appbar的setDisplayHomeAsUpEnabled()方法。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my_child);

    // my_child_toolbar is defined in the layout file
    Toolbar myChildToolbar =
        (Toolbar) findViewById(R.id.my_child_toolbar);
    setSupportActionBar(myChildToolbar);

    // Get a support ActionBar corresponding to this toolbar
    ActionBar ab = getSupportActionBar();

    // Enable the Up button
    **ab.setDisplayHomeAsUpEnabled(true);**
}

我們不需要在onOptionsItemSelected()處理Up按鈕的事件,我們只需要調用這個方法的父類方法即可,即super.onOptionsItemSelected()。因為系統已經通過manifest中的定義自動處理了這個事件。

Action 視圖(View)和Action 提供者(Provider)

Action View 是在Appbar上提供功能豐富的action。比如一個搜索action,它可以在appbar輸入搜索內容,而不改變Activity或Fragment的樣式 Action Provider是一個有自定義布局的action。這個action一開始是一個button或menu,但用戶點擊了action後,你可以通過action provider任意控制你定義的action的行為。

添加一個Action View

在toolbar的菜單資源文件中添加一個item來添加一個ActionView,比如搜索框SearchView的定義:

action_view
圖:Toolbar上的SearchView運行效果。

也可以配置action,通過getActionView()得到actionview的對象,然後進行操作。例如SearchView:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_activity_actions, menu); MenuItem searchItem = menu.findItem(R.id.action_search); SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); // Configure the search info and add any event listeners... return super.onCreateOptionsMenu(menu); }

響應action view的伸展行為

如果菜單的item中設置了collapseActionView 標記,則這個action View會在appbar上顯示一個icon,那麼點擊這個icon,這個actionview就會展開,同樣也可以縮回來,展開和縮回的行為我們可以為它設置監聽。我們使用MenuItem.OnActionExpandListener來監聽,我們可以在裡面處理展開和縮回來時改變Activity的UI:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.options, menu);
    // ...

    // Define the listener
    OnActionExpandListener expandListener = new OnActionExpandListener() {
        @Override
        public boolean onMenuItemActionCollapse(MenuItem item) {
            // Do something when action item collapses
            return true;  // Return true to collapse action view
        }

        @Override
        public boolean onMenuItemActionExpand(MenuItem item) {
            // Do something when expanded
            return true;  // Return true to expand action view
        }
    };

    // Get the MenuItem for the action item
    MenuItem actionMenuItem = menu.findItem(R.id.myActionItem);

    // Assign the listener to that action item
    MenuItemCompat.setOnActionExpandListener(actionMenuItem, expandListener);

    // Any other things you have to do when creating the options menu…

    return true;
}

添加一個Action Provider

在菜單的item中添加actionProviderClass屬性來添加一個action provider。例如,我們定義一個ShareActionProvider如下:

這裡不需要為ShareActionProvider提供一個icon因為系統已經定義了,不過可以自定義一個icon,Just do it!

使用Snackbar代替Toast

參考地址:http://developer.android.com/training/snackbar/index.html
很多時候我們都需要短暫的彈出一個消息提示用戶,然後自動消失。Android以前是用Toast類來實現的,現在我們偏向於首選使用Snackbar組件來代替Toast實現這樣的需求,當然,Toast依然是支持的。

創建和顯示一個Pop-Up 消息

Snackbar組件是彈出消息的理想選擇。

使用CoordinatorLayout

Snackbar是綁定在CoordinatorLayout上的,而且增加了一些新特性:

Snackbar可以通過手勢滑動dismiss掉 Snackbar顯示時將移動在它上面的布局。
CoordinatorLayout類提供FrameLayout功能的超集,所以如果你使用了FrameLayout,Just將其換成CoordinatorLayout,因為CoordinatorLayout提供了Snackbar的功能。如果你的布局采用了其他的布局方式,下面展示將你的布局包在CoordinatorLayout之中:


    
    

        

    

注意:必須為CoordinatorLayout設置android:id屬性,因為Snackbar顯示pop消息需要CoordinatorLayout的id。

顯示一個消息

2步:1、創建Snackbar對象;2、調用show方法

Snackbar mySnackbar = Snackbar.make(viewId, stringId, duration);

viewId一般參入與之綁定的CoordinatorLayout的layoutId。

mySnackbar.show();

系統在同一時間不能顯示多個Snackbar。所以要顯示第二個Snackbar,要等第一個Snackbar過期或被dismiss。
如果不用調用Snackbar其他實用方法,僅僅是顯示一條消息而不用持有Snackbar的引用,你也可以將創建和顯示一起寫:

Snackbar.make(findViewById(R.id.myCoordinatorLayout), R.string.email_sent,
                        Snackbar.LENGTH_SHORT)
        .show();

在消息中添加一個Action

可以在Snackbar中添加一個Action。比如加一個undo按鈕,那麼在刪除一個郵件之後,可以點擊這個undo按鈕恢復剛剛刪除的郵件。
snackbar_undo_action
為Snackbar中的button設置事件監聽,使用Snackbar的setAction()方法:

public class MyUndoListener implements View.OnClickListener{

    &Override
    public void onClick(View v) {

        // Code to undo the user's last action
    }
}

Snackbar mySnackbar = Snackbar.make(findViewById(R.id.myCoordinatorLayout),
                                R.string.email_archived, Snackbar.LENGTH_SHORT);
mySnackbar.setAction(R.string.undo_string, new MyUndoListener());
mySnackbar.show();

注意:Snackbar 只是短暫的顯示一個消息,你不能指望用戶看到消息時還有機會按到這個按鈕。所以,你得考慮另一種方法去執行這個action。

自定義View(一) - 定義自己的View類

參考地址:http://developer.android.com/training/custom-views/create-view.html
自定義View的第一種方式是定義自己的View類,並為它設置自定義的屬性。

創建View的子類

繼承View必須至少有一個裡面有Context和AttributeSet的構造方法。

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

定義自定義屬性

給自定義的View力添加自定義的屬性,從而在XML布局時使用,有如下步驟:
1、在資源文件中添加自定義屬性
2、在XML布局中為自定義屬性賦值
3、在運行時提取出屬性的值
4、將屬性的值應用到自定義的View上

例如,下面是res/values/attrs.xml的例子:


   
       
       
           
           
       
   

上面的代碼定義了showTextlabelPosition的屬性,他倆屬於名為PieChart的styleable。 styleable實體的名稱一般和自定義View的名稱相同,但這不是必須的。
定義好了自定義屬性,就要去自己的布局文件中使用了,必須聲明命名空間。http://schemas.android.com/apk/res/[your package name]。比如:



 

注意:自定義View在XML布局中使用,必須使用完整的包名+類名;如果自定義的View是一個類的內部類,那麼需要從它的外部類訪問它了。比如,這個例子中的自定義View是PieView,是PieChart的內部類,則這樣使用它:com.example.customviews.charting.PieChart$PieView

應用自定義屬性

雖然我們可以從AttributeSet 中讀出屬性值,但有兩個不好的地方:

資源強引用屬性值 不支持Style
所以,我們不使用AttributeSet而使用obtainStyledAttributes()得到一個TypedArray。TypedArray裡面的屬性值解除了引用,而且被樣式化(styled)了。例如:
public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0);

   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       **a.recycle();**
   }
}

注意:TypedArray不是共享的資源,在使用完後必須手動釋放。

增加屬性和事件

為自定義View的類中的屬性增加get和set方法,以動態的改變View的外觀和行為。例如:

public boolean isShowText() {
   return mShowText;
}

public void setShowText(boolean showText) {
   mShowText = showText;
   invalidate();
   requestLayout();
}

注意:setShowText調用了invalidate()requestLayout(),這個確保能讓View正確的展示。改變View的屬性後需要調用invalidate以及時刷新,同樣如果View屬性改變影響尺寸和形狀的話也要調用requestLayout,否則會產生一些難以捕捉的bug。

輔助性設計

我們的App需要為一些有殘障的人士使用,那麼我們需要做一些額外的工作:

在輸入控件上添加android:contentDescription屬性。(視力障礙者使用Google的一些服務可以通過聲音讀出來) 在合適的地方調用sendAccessibilityEvent()發送輔助的事件 支持備用控制器,方向鍵和軌跡球等

自定義View(二) - 自定義繪制

參考地址:http://developer.android.com/training/custom-views/custom-drawing.html
自定義View的最重要的部分就是繪制外觀(重寫onDraw()),而根據應用程序的不同需求,繪制可易可難。

創建繪制對象

canvas,表示畫什麼;paint,表示怎麼畫。

private void init() {
   mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mTextPaint.setColor(mTextColor);
   if (mTextHeight == 0) {
       mTextHeight = mTextPaint.getTextSize();
   } else {
       mTextPaint.setTextSize(mTextHeight);
   }

   mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mPiePaint.setStyle(Paint.Style.FILL);
   mPiePaint.setTextSize(mTextHeight);

   mShadowPaint = new Paint(0);
   mShadowPaint.setColor(0xff101010);
   mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

   ...

提前創建Paint對象是一個重要的優化。View經常需要不斷的重繪(reDraw),在onDraw中創建Paint對象將嚴重降低性能,有可能使界面卡頓。

處理布局事件

為了正確地畫出你的自定義View,您需要知道它的大小。復雜的自定義View通常需要根據自己在屏幕上的區域大小和形狀執行多次布局計算。對於View大小你不能猜測要進行明確的計算。即使只有一個應用程序使用你的View,應用程序需也要處理不同的屏幕尺寸,多個屏幕密度以及不同寬高比、橫屏豎屏等情況。
盡管View有很多方法處理測量行為,其中大部分是不需要覆蓋的。如果你認為不需要特殊控制其大小,你只需要覆蓋一個方法:onSizeChanged()
當你的View初次分配一個size時會調用onSizeChanged(),當因為任何理由改變了size都會再次調用。在onSizeChanged()中它會計算位置、尺寸、和任何與View有關的值,而不是在onDraw()中進行重新計算。在PieChart的例子中,onSizeChanged()就是PieChart的View用來計算的邊界矩形餅圖和文本標簽以及其他可見UI元素的相對位置的。
當你的View被分配一個size,布局管理器會假定size裡包含了包括了所有View的Padding。你必須處理Padding的值來計算你的View的大小。在例子中PieChart.onSizeChanged()是這樣做的:

  // Account for padding
       float xpad = (float)(getPaddingLeft() + getPaddingRight());
       float ypad = (float)(getPaddingTop() + getPaddingBottom());

       // Account for the label
       if (mShowText) xpad += mTextWidth;

       float ww = (float)w - xpad;
       float hh = (float)h - ypad;

       // Figure out how big we can make the pie.
       float diameter = Math.min(ww, hh);

如果你要更好的控制你的布局參數(Layout Parameters),需要實現onMeasure()方法。這個方法的參數是View.MeasureSpec的值,這個值告訴你的View的父視圖希望你的View多大,或者允許你的View最大是多少甚至建議是多少。作為一個優化,這些值通過包裝的int整型數值存儲,然後使用View.MeasureSpec的靜態方法拆包出存儲在每個int值裡的信息。
下面是onMeasure()的一個例子,PieChart 試圖讓自己的view區域足夠大以至讓餅圖和文本一樣大。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width ends up being, ask for a height that would let the pie
   // get as big as it can
   int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

上面的代碼有三個重要的點:

將padding值參與了計算,前面提到的這是view的責任。 方法resolveSizeAndState()用於幫助決定最終的寬度和高度值。這個方法通過比較view期望得到的值和onMeasure規則指定的值返回一個View.MeasureSpec的值。 onMeasure()方法沒有返回值。相反,它通過調用setMeasuredDimension()傳遞算出來的結果。setMeasuredDimension方法調用是必須的,如果你忽略了,系統會報一個運行時異常。

Draw!!

接下來就是繪制了。每個View的繪制方法都不一樣,但有一些共同的操作是一樣的:

使用drawText()繪制文本。通過setTypeface()設置字體,setColor()設置文本顏色 使用drawRect(), drawOval(), 和drawArc()繪制原始圖形。setStyle()方法控制幾何圖形如何被解析。 使用drawPath()繪制復雜的圖形。 使用LinearGradient對象定義線性填充。 使用drawBitmap()繪制bitmap
PieChart中繪制了文本、線條以及圖形:
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);

   // Draw the shadow
   canvas.drawOval(
           mShadowBounds,
           mShadowPaint
   );

   // Draw the label text
   canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);

   // Draw the pie slices
   for (int i = 0; i < mData.size(); ++i) {
       Item it = mData.get(i);
       mPiePaint.setShader(it.mShader);
       canvas.drawArc(mBounds,
               360 - it.mEndAngle,
               it.mEndAngle - it.mStartAngle,
               true, mPiePaint);
   }

   // Draw the pointer
   canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
   canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}

示例下載:PieChart。

自定義View(三) - 添加View事件模擬現實

參考地址:http://developer.android.com/training/custom-views/making-interactive.html
繪制了View之後,我們需要給View添加一些事件,讓它更接近於現實。比如滑動一個View,當快速滑動並突然放手時,View會因為慣性繼續滑動。本文介紹通過AndroidFramework來給自定義的View添加“現實世界”的事件。

手勢事件

Android中最多最常見的事件就是touch事件。我們可以在View的onTouchEvent(android.view.MotionEvent)方法中處理:

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

現代的touch UI事件是由手勢tapping、pulling、pushing、flinging和flinging定義的。將原始的觸摸事件轉換為手勢,Android中使用GestureDetector。
創建GestureDetector需要實現GestureDetector.OnGestureListener接口,如果你只需要處理一些手勢,可以繼承GestureDetector.SimpleOnGestureListener。

class mListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());

不管你是否使用GestureDetector.SimpleOnGestureListener,你總需要在onDown()中返回true。因為如果返回false,系統會認為你要忽略其他手勢,GestureDetector.OnGestureListener的其他方法將都得不到調用。返回false的情況只有一種,就是你真的要忽略掉其他手勢(情況極少)。
創建完GestureDetector之後就要在onTouchEvent().中攔截觸摸事件了:

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = mDetector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

在onTouchEvent()中,如果一些觸摸事件不能識別為手勢事件,則返回false,我們可以用自己的代碼來檢測手勢。

仿生運動

Android提供Scroller類來處理慣性這種手勢事件。
當快速滑動時,在fling()中使用啟動速度和最小和最大x和y的值。對於速度,你可以使用GestureDetector計算出來的值。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
}

注意:盡管GestureDetector本身計算速度比較准確,但許多開發人員認為使用這個值還是太大,所以一般將x和y速度除以4到8倍。
fling()的調用是建立在快速滑動手勢的物理模型上的。然後你需要每隔一定時間調用Scroller.computeScrollOffset()來更新Scroller。Scroller.computeScrollOffset()通過讀取當前時間以及在當前時間使用物理模型計算出的x、y值來更新Scroller的內部狀態。調用getCurrX()和getCurrY()可以提取到這些值。
大多數View直接傳遞Scroller的x、y值給scrollTo()。PieChart例子有一點不同,他是用當前Scroller的y值來設置圖表的旋轉角。

if (!mScroller.isFinished()) {
    mScroller.computeScrollOffset();
    setPieRotation(mScroller.getCurrY());
}

Scroller類幫你計算滾動位置,但它不能自動幫你應用這些位置到你的View上。所以必須由你確保將最新獲得的坐標及時應用到滾動動畫上,讓它看起來很平滑。有兩種方式:

在onFling()後調用postInvalidate()以強制重繪。這樣會在onDraw中重新計算滾動偏移量 在fling的時候創建一個ValueAnimator進行動畫,並調用addUpdateListener()添加一個監聽來處理動畫更新
PieChart 例子使用的第二種方案。這個技術使用起來稍微復雜點,但它運行更接近於動畫系統,並且不會帶來潛在的View的無效刷新。缺點是ValueAnimator是Android3.0(API 11)才出來的,以前的老版本用不了。

注意:為系統兼容性,你需要在使用ValueAnimator的地方判斷系統版本號。

  mScroller = new Scroller(getContext(), null, true);
       mScrollAnimator = ValueAnimator.ofFloat(0,1);
       mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator valueAnimator) {
               if (!mScroller.isFinished()) {
                   mScroller.computeScrollOffset();
                   setPieRotation(mScroller.getCurrY());
               } else {
                   mScrollAnimator.cancel();
                   onScrollFinished();
               }
           }
       });

平滑過渡

用戶希望UI變換時有個平滑的過渡,而不是突然變化。在Android3.0以後,Android使用 property animation framework,可以很容易的處理平滑過渡問題。
當一個View的屬性改變而導致它的展示發生改變時,我們可以使用ValueAnimator來做,這樣不會使改變太唐突。例如:

mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();

如果你想要改變View基本屬性,做動畫更容易,因為View有一個內置的ViewPropertyAnimator,它對多個屬性同時動畫進行了優化。如:

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();

PieChart下載。

自定義View(四) - 優化View性能

參考地址:http://developer.android.com/training/custom-views/optimizing-view.html
來自官網的優化View的描述:
現在,你已經有了一個精心設計的View,我們要確保使用手勢以及狀態之間的切換運行流暢,避免UI卡頓;使動畫始終運行在每秒60幀以保證動畫運行不斷斷續續。
為加速你的View,在onDraw()中不要添加無意義的代碼,因為這個方法執行的非常頻繁。在onDraw()中也不要有分配內存的操作,因為每一次內存操作都會引發潛在的GC操作,從而導致卡頓。永遠不要在運行動畫時分配內存。
保證onDraw()代碼簡潔,從而讓它盡可能快的執行。盡可能消除不必要的invalidate()來減少onDraw()的執行。

另一個耗時的操作是遍歷布局。任何時候,view調用requestLayout,Android UI系統都要遍歷整個view的層級結構找到每個View到底應該多大,如果遇到沖突的測量值,它可能重復幾次遍歷整個View層級。

UI設計師經常設計的很深的View的層級,嵌在ViewGroup裡,來方便實現需求。很深的View層級會帶來性能問題。請保證你的View層級越淺越好。

如果你有一個很復雜的UI,請自定義一個ViewGroup來實現它。不像內置的View,你自定義的View可以特定自己子View的形狀和大小,而避免遍歷所有的子View並測量其size和shape。PieChart 例子展示了如何繼承於ViewGroup作為自定義View,PieChart 有子View,但從未測量過它們,它是通過自定義布局的算法給子View設置特定的Size。

向下兼容

參考地址:http://developer.android.com/training/backward-compatible-ui/index.html
本文用 Android3.0以後才有的Action Bar Tabs的例子,展示如何用抽象的方法做向下兼容。

抽象出Tab的接口

我們假設要設計出一個頂部的Tab選項卡,有如下需求:

Tab選項卡由文本和icon組成 每一個Tab都和一個Fragment對應 Activity能響應Tab切換的事件
本文使用Eclair (API level 5) 和 Honeycomb (API Level 11)兩個系統版本來做例子討論如何使用抽象做向下兼容。下面這張圖顯示了類的抽象和接口的設計:
backward-compatible-ui-classes

將ActionBar.Tab抽象

我們模仿ActionBar.Tab類中的方法進行抽象,相當於我們定義了自己的兼容Tab類

public abstract class CompatTab {
    ...
    public abstract CompatTab setText(int resId);
    public abstract CompatTab setIcon(int resId);
    public abstract CompatTab setTabListener(
            CompatTabListener callback);
    public abstract CompatTab setFragment(Fragment fragment);

    public abstract CharSequence getText();
    public abstract Drawable getIcon();
    public abstract CompatTabListener getCallback();
    public abstract Fragment getFragment();
    ...
}

然後創建一個抽象類,允許你創建和添加Tab,類似於ActionBar.newTab() 和ActionBar.addTab():

public abstract class TabHelper {
    ...

    public CompatTab newTab(String tag) {
        // This method is implemented in a later lesson.
    }

    public abstract void addTab(CompatTab tab);

    ...
}

我們創建的CompatTab 類和TabHelper類是一種代理模式的實現。你可以在這些具體類中使用更新的API而不會導致設別crash,因為比如只要你不在Honeycomb (API Level 11)之前的設備上使用Honeycomb的API,系統就不會報VerifyError異常。
一種比較好的實現方法是用版本號命名定義這些抽象類的實現類,比如使用CompatTabHoneycomb和TabHelperHoneycomb來實現在Android3.0上的設備使用Tab的實現:
backward-compatible-ui-classes-honeycomb

實現CompatTabHoneycomb

我們使用新的ActionBar.Tab的API來實現CompatTabHoneycomb:

public class CompatTabHoneycomb extends CompatTab {
    // The native tab object that this CompatTab acts as a proxy for.
    ActionBar.Tab mTab;
    ...

    protected CompatTabHoneycomb(FragmentActivity activity, String tag) {
        ...
        // Proxy to new ActionBar.newTab API
        mTab = activity.getActionBar().newTab();
    }

    public CompatTab setText(int resId) {
        // Proxy to new ActionBar.Tab.setText API
        mTab.setText(resId);
        return this;
    }

    ...
    // Do the same for other properties (icon, callback, etc.)
}

實現TabHelperHoneycomb

直接使用代理調用ActionBar的API:

public class TabHelperHoneycomb extends TabHelper {
    ActionBar mActionBar;
    ...

    protected void setUp() {
        if (mActionBar == null) {
            mActionBar = mActivity.getActionBar();
            mActionBar.setNavigationMode(
                    ActionBar.NAVIGATION_MODE_TABS);
        }
    }

    public void addTab(CompatTab tab) {
        ...
        // Tab is a CompatTabHoneycomb instance, so its
        // native tab object is an ActionBar.Tab.
        mActionBar.addTab((ActionBar.Tab) tab.getTab());
    }

    // The other important method, newTab() is part of
    // the base implementation.
}

接下來我們就要實現在舊的版本設備上實現一樣的Tab了,需要尋找一個替代的解決方案:

使用Older APIs實現Tabs

我們使用TabWidget和TabHost 作為替代方案實現TabHelperEclair和CompatTabEclair,因為TabWidget和TabHost的API在Android 2.0 (Eclair)以後就有了。
backward-compatible-ui-classes-eclair

CompatTabEclair實現了一個文本和icon的存儲,因為在Eclair版本上沒有ActionBar.Tab的API:

public class CompatTabEclair extends CompatTab {
    // Store these properties in the instance,
    // as there is no ActionBar.Tab object.
    private CharSequence mText;
    ...

    public CompatTab setText(int resId) {
        // Our older implementation simply stores this
        // information in the object instance.
        mText = mActivity.getResources().getText(resId);
        return this;
    }

    ...
    // Do the same for other properties (icon, callback, etc.)
}

TabHelperEclair類的實現是使用TabHost組件創建TabHost.TabSpec 實現:

public class TabHelperEclair extends TabHelper {
    private TabHost mTabHost;
    ...

    protected void setUp() {
        if (mTabHost == null) {
            // Our activity layout for pre-Honeycomb devices
            // must contain a TabHost.
            mTabHost = (TabHost) mActivity.findViewById(
                    android.R.id.tabhost);
            mTabHost.setup();
        }
    }

    public void addTab(CompatTab tab) {
        ...
        TabSpec spec = mTabHost
                .newTabSpec(tag)
                .setIndicator(tab.getText()); // And optional icon
        ...
        mTabHost.addTab(spec);
    }

    // The other important method, newTab() is part of
    // the base implementation.
}

現在有兩個CompatTab和TabHelper的實現:一個運行在Android 3.0或更高版本,並使用新的api;另一個運行Android 2.0或更高版本,並使用老的api。下面將討論在應用程序中使用這些實現:

添加切換邏輯

TabHelper 類扮演了一個工廠,由他創建兼容各種設備的TabHelper 和CompatTab實例:

public abstract class TabHelper {
    ...
    // Usage is TabHelper.createInstance(activity)
    public static TabHelper createInstance(FragmentActivity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            return new TabHelperHoneycomb(activity);
        } else {
            return new TabHelperEclair(activity);
        }
    }

    // Usage is mTabHelper.newTab("tag")
    public CompatTab newTab(String tag) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            return new CompatTabHoneycomb(mActivity, tag);
        } else {
            return new CompatTabEclair(mActivity, tag);
        }
    }
    ...
}

創建版本兼容的布局(layout)

在Android3.0以上我們使用ActionBar,而在2.0以上我們使用TabHost,所以在布局上使用老版本時我們需要在XML布局中定義TabHost和TabWidget。
res/layout/main.xml:




    

        

        <framelayout android:id="@android:id/tabcontent" android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent">

    </framelayout>

在Android3.0以後的布局是這樣的:
res/layout-v11/main.xml:

<framelayout android:id="@android:id/tabcontent" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"></framelayout>

系統運行時,Android會根據系統本身的版本自動選擇main.xml布局文件。

在Activity中使用TabHelper

@Override
public void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.main);

    TabHelper tabHelper = TabHelper.createInstance(this);
    tabHelper.setUp();

    CompatTab photosTab = tabHelper
            .newTab("photos")
            .setText(R.string.tab_photos);
    tabHelper.addTab(photosTab);

    CompatTab videosTab = tabHelper
            .newTab("videos")
            .setText(R.string.tab_videos);
    tabHelper.addTab(videosTab);
}

下面是兩個分別運行在Android 2.3和Android 4.0設備的截圖:
backward-compatible-ui-gb
backward-compatible-ui-ics

代碼示例下載:TabCompat.zip

實現輔助(外掛)

參考地址:http://developer.android.com/training/accessibility/index.html
我們設計開發的App需要給更廣泛的人群使用,有一部分的在視力、手腳等方面可能有殘障,這時為了提高他們這部分人的用戶體驗,我們需要使用輔助服務來幫助他們來完成交互。也就是Android Framework中定義的Accessibility Services。

設計輔助程序

添加android:contentDescription屬性

利用Google基於聲音的TalkBack服務 ,在UI元素上加 android:contentDescription屬性,可以使用這個服務將其讀出來,使得視力障礙者可以通過聲音訪問這些元素。例如:

有一些有狀態的UI(比如ToggleButton,CheckBox),就不能通過在布局中設置android:contentDescription來實現,這時可以在代碼中動態設置:

String contentDescription = "Select " + strValues[position];
label.setContentDescription(contentDescription);

代碼很簡單,但是卻很有用。下載TalkBack服務,然後在 Settings > Accessibility > TalkBack中開啟服務即可使用。

Android不僅僅提供了觸摸屏的導航方式,還可以通過D-Pad、方向鍵或軌跡球來操作。後來Android還提供了通過USB或藍牙連接的外置鍵盤來操作。
要使用這種形式的操作方式,必須讓設置操作的元素處於獲取焦點狀態(Focus),使用View.setFocusable()或在XML布局中設置 android:focusable屬性。
另外,每個UI控件都有四個屬性android:nextFocusUp, android:nextFocusDown, android:nextFocusLeftandroid:nextFocusRight,你可以用這些屬性定義在某個方向哪個控件將獲取焦點,因為系統默認是采用布局臨近原則來自動決定順序的,采用這四個屬性可以人工干預。
例如:有一個Button和一個TextView,都可以focus,當按下方向鍵的時候焦點從button跳到TextView,按上焦點返回到button:

最好的驗證方式是在模擬器上,操作上下方向鍵,來查看控件的焦點情況。

發送輔助事件

如果你使用AndroidFramework中的控件,那麼不管何時它的選中狀態或焦點狀態發生變化,都可以發送AccessibilityEvent。這個事件是由accessibility service檢查的,可以提供像TTS那樣的功能。
如果你寫一個自定義的View,要保證在適當的時候發送accessibility event。通過調用sendAccessibilityEvent(int)方法,其中參數代表發生的事件類,來創建一個事件。AccessibilityEvent提供了完整的事件類型列表。
例如,你想繼承imageview以致於在它獲取焦點時可以通過鍵盤輸入標題上去,這時需要發生一個 TYPE_VIEW_TEXT_CHANGED事件,盡管這個事件一般沒有在內置定義在imageview中。代碼如下:

public void onTextChanged(String before, String after) {
    ...
    if (AccessibilityManager.getInstance(mContext).isEnabled()) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    }
    ...
}

開發一個Accessibility Service

創建一個Accessibility Service

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

在Manifest中注冊Service,要特別指定android.accessibilityservice,當應用程序發出一個AccessibilityEvent時可以接收到。


...

     
         
     
     . . .

...

配置Accessibility Service

配置Accessibility Service有兩種方式,兼容的方式是在代碼中配置。在onServiceConnected()中調用setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo)來配置輔助服務:

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to.  Others
    // won't be passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific applications, set their
    // package names here.  Otherwise, when the service is activated, it will listen
    // to events from all applications.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

    // Set the type of feedback your service will provide.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

    // Default services are invoked only if no package-specific ones are present
    // for the type of AccessibilityEvent generated.  This service *is*
    // application-specific, so the flag isn't necessary.  If this was a
    // general-purpose service, it would be worth considering setting the
    // DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

從Android4.0開始,我們可以將配置寫在一個XML文件中,一些配置選項比如canRetrieveWindowContent只能在XML中配置。和上面代碼同樣的配置選項的XML配置如下:

如果使用XML配置,還需要在Manifest文件中配置 屬性,指定輔助服務的resource為上面的XML配置文件:


     
         
     
     

響應AccessibilityEvents

覆蓋onAccessibilityEvent(AccessibilityEvent)方法來處理AccessibilityEvents事件:

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Focused: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string
    // back to the user.
    speakToUser(eventText);
    ...
}

查詢View Heirarchy獲取的Context

這個是 Android 4.0 (API Level 14) 上AccessibilityService 才有的能力,這種能力非常有用!我們需要在XML的配置中配置android:canRetrieveWindowContent=”true”
通過getSource()獲得AccessibilityNodeInfo對象,如果事件源的窗口仍然是活動窗口,則這個調用返回一個對象;否則返回null。
下面的例子是一個代碼片段,它接收到一個事件時,作如下事情:
1、直接抓住事件源View的父視圖
2、在父View裡,尋找一個label和checkbox作為子View
3、如果找到了,創建一個string發給用戶,標識checkbox是否被選中
4、如果遍歷了整個view hierarchy返回null,則默默的放棄


// Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fired the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label and the checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

現在,你有一個功能完整的accessibility servicel了。試著配置TTS引擎來更好的與用戶交互,或者使用振動提供觸摸反饋。

系統狀態欄和導航欄

參考地址:http://developer.android.com/training/system-ui/index.html

變暗系統狀態欄

在Android 4.0(API14)及以上可以使用SYSTEM_UI_FLAG_LOW_PROFILE這個Flag很容易的變暗狀態欄。Android早期版本系統不提供一個內置的API變暗狀態欄。

// This example uses decor view, but you can use any visible view.
View decorView = getActivity().getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE;
decorView.setSystemUiVisibility(uiOptions);

當用戶觸摸狀態或導航欄,這個flag就消失了,就恢復了明亮。如果想再次變暗它,就需要重新設置它。
如果你要通過代碼清除flag,使用setSystemUiVisibility():

View decorView = getActivity().getWindow().getDecorView();
// Calling setSystemUiVisibility() with a value of 0 clears
// all flags.
decorView.setSystemUiVisibility(0);

隱藏狀態欄

status_bar_show
注意:狀態欄不可見時,永遠不要顯示action bar
status_bar_hide

在Android 4.0及以下版本上隱藏狀態欄


    ...

或者

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // If the Android version is lower than Jellybean, use this call to hide
        // the status bar.
        if (Build.VERSION.SDK_INT < 16) {
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }
        setContentView(R.layout.activity_main);
    }
    ...
}

可以使用FLAG_LAYOUT_IN_SCREEN這個flag設置你的Activity使用相同的屏幕區域,這樣就不會使狀態欄不停的隱藏和顯示了。

在Android 4.1隱藏狀態欄

View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.hide();

注意:設置UI的flag只是當時生效。比如你在onCreate()中設置隱藏狀態欄,點擊home回到桌面狀態欄顯示,再次進入之後onCreate()不會再執行,狀態欄就一直顯示,就會有問題了。解決方法是:在onResume()或onWindowFocusChanged()中設置flag使其消失。
setSystemUiVisibility()方法只對可見的View有效
設置過setSystemUiVisibility()的View再導航離開後,flag會消失。

讓界面內容顯示在狀態欄的後面

在Android 4.1及以後,可以設置界面內容在狀態欄的後面,這樣界面就不會因為狀態欄顯示和隱藏而resize了。使用SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN這個flag就可以了,還可以使用SYSTEM_UI_FLAG_LAYOUT_STABLE這個flag幫助app維持一個穩定的布局。當你使用這種方法時,你就要對它負責,來確保你的某些UI(比如地圖的內置控件)不會被遮住而影響使用。多數情況下你可以在XML布局中設置android:fitsSystemWindows屬性為true來處理這種情況,這對大多數應用都適用。
某些情況下,可能你需要修改默認的padding值來得到想要的合理布局。要直接操作內容布局相對於狀態欄的位置(占據的那部分空間稱content insets),需要覆蓋fitSystemWindows(Rect insets)方法。fitSystemWindows方法在content insets發生變化被 view hierarchy時調用,允許window調整它的content。通過覆蓋這個方法,不管你想不想,你都可以處理這個insets 。

讓Actionbar和狀態欄同步

在Android 4.1及以上版本中,為避免在actionbar隱藏和顯示時resize你的布局,你可以為actionbar開啟覆蓋(overlay)模式。在覆蓋模式中,你的Activity使用盡可能大的空間好像Actionbar不在那兒一樣,其實actionbar是在布局的上面,只是布局頂部有一部分變模糊了,但現在actionbar不管顯示和隱藏,都不會resize布局了。
打開覆蓋模式,需要創建一個自定義的主題,繼承一個帶有actionbar的主題,設置 android:windowActionBarOverlay=true。然後使用上面提到的SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN Flag,設置你的Activity在打開SYSTEM_UI_FLAG_FULLSCREEN flag時使用相同的屏幕區域。當你要隱藏SystemUI時,使用SYSTEM_UI_FLAG_FULLSCREEN的flag。這個也會隱藏action bar(因為android:windowActionBarOverlay=true),而且在隱藏和顯示時有一個和諧的動畫。

隱藏導航欄

navigation-bar
隱藏導航欄,但我們要設計成讓它容易訪問到它。給用戶提供一個更加沉浸式的用戶體驗。你可以在Android4.0(API 14)
及以上版本,使用SYSTEM_UI_FLAG_HIDE_NAVIGATION的flag來隱藏導航欄。

View decorView = getWindow().getDecorView();
// Hide both the navigation bar and the status bar.
// SYSTEM_UI_FLAG_FULLSCREEN is only available on Android 4.1 and higher, but as
// a general rule, you should design your app to hide the status bar whenever you
// hide the navigation bar.
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
              | View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);

使用這個方法,用戶點擊屏幕任何地方將導致導航欄(和狀態欄)都重新顯示並保持。
這個flag被清除後,需要重新設置它進行隱藏導航欄
其他部分都和狀態欄的注意部分一樣

讓界面內容顯示在導航欄下面

在Android4.1及以上版本,使用SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION的flag使界面布局顯示在導航欄的下面,並使用SYSTEM_UI_FLAG_LAYOUT_STABLE保持布局。
其他部分和狀態欄部分注意部分相同

使用沉浸式的全屏模式

Android 4.4 (API Level 19)中為setSystemUiVisibility()新介紹了SYSTEM_UI_FLAG_LAYOUT_STABLE的flag,它讓你的app真實的進入“全屏”模式,和SYSTEM_UI_FLAG_HIDE_NAVIGATION以及SYSTEM_UI_FLAG_FULLSCREEN結合起來時,隱藏狀態欄和導航欄,app將捕獲全屏的觸摸事件。
當沉浸式全屏模式開啟後,你的Activity持續的接收全屏的觸摸事件。當用戶沿著system bar一般顯示的地方向內滑動時會讓system bar顯示出來。這個動作清除了SYSTEM_UI_FLAG_HIDE_NAVIGATION flag (以及 SYSTEM_UI_FLAG_FULLSCREEN,如果應用的話),於是system bar變得可見,這會觸發View.OnSystemUiVisibilityChangeListener。然而,你希望system bar一會兒後再自動隱藏,你可以使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY的flag。注意這個粘性(”sticky” )的版本不會觸發任何監聽事件,因為system bar在這種模式下只是暫時性的顯示。
imm-states

非沉浸式模式。在app進入沉浸式模式前的狀態。它也表示如果你使用沉浸式flag,當用戶滑動時清除了SYSTEM_UI_FLAG_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_FULLSCREEN顯示system bar的情況。這是保持UI控件和system bar同步的最好實踐,它最小化了屏幕的狀態數。這個提供了更加無縫的用戶體驗,所以這裡所有的UI控件和狀態欄一起顯示。一旦進入沉浸模式,UI控件將隨著system bar的隱藏而隱藏。為確保你的UI和system bar保持可見,使用View.OnSystemUiVisibilityChangeListener監聽可見性的變化。 提示氣泡。當用戶第一次進入沉浸模式時,系統將顯示一個提示氣泡。這個氣泡提示用戶將怎樣顯示system bar。注意:如果你想強制性的顯示提示氣泡用作測試意圖,你可以將app進入沉浸模式,然後關閉屏幕,然後在5秒內點亮屏幕。 沉浸模式。app進入沉浸模式,system bars和其他UI控件都隱藏。 粘性Flag。這個UI是你使用IMMERSIVE_STICKY的Flag,然後用戶滑動使system bar顯示的。半透明的bar臨時顯示然後會再隱藏。滑動行為不會清除任何flag,所以也不會觸發system UI可見性變化的監聽。

注意:沉浸的FLag只有你使用SYSTEM_UI_FLAG_HIDE_NAVIGATION, SYSTEM_UI_FLAG_FULLSCREEN的Flag或兩者都有的時候才會生效。通常情況下當你使用“全屏沉浸”模式會隱藏狀態欄和導航欄。

SYSTEM_UI_FLAG_IMMERSIVESYSTEM_UI_FLAG_IMMERSIVE_STICKY可以提供一個差異化的沉浸式的體驗。
下面是一些情況,你需要使用其中一個,而不是另一個:

當你開發一個閱讀app,新聞app或雜志app時,使用沉浸flag需要和SYSTEM_UI_FLAG_FULLSCREEN、 SYSTEM_UI_FLAG_HIDE_NAVIGATION兩者結合起來用。 當你開發一個完全沉浸模式的app,期望用戶和屏幕的邊緣進行交互而不期望用戶頻繁的和system UI交互,使用粘性沉浸的flag,結合SYSTEM_UI_FLAG_FULLSCREEN 和SYSTEM_UI_FLAG_HIDE_NAVIGATION使用。 如果你開發一個視頻播放器或其他很少需要用戶交互的app,你可能需要老一點版本的方法了( Android 4.0 (API Level 14)及以上)。因為對於這類app,簡單的使用SYSTEM_UI_FLAG_FULLSCREEN和會SYSTEM_UI_FLAG_HIDE_NAVIGATION的Flag就足夠了,不需要沉浸的flag。

使用非粘性沉浸

這段代碼演示了如何隱藏和顯示狀態欄和導航欄,而不用resize界面的內容。

// This snippet hides the system bars.
private void hideSystemUI() {
    // Set the IMMERSIVE flag.
    // Set the content to appear under the system bars so that the content
    // doesn't resize when the system bars hide and show.
    mDecorView.setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
            | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
            | View.SYSTEM_UI_FLAG_IMMERSIVE);
}

// This snippet shows the system bars. It does this by removing all the flags
// except for the ones that make the content appear under the system bars.
private void showSystemUI() {
    mDecorView.setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}

1、注冊一個監聽讓你的app得到system UI可見性變化的通知。
2、實現onWindowFocusChanged()方法。如果你獲得window的焦點,你可能想重新隱藏system bar。如果你失去了window的焦點,例如一個對話框或彈出菜單,你可能想取消之前的Handler.postDelayed()或類似方法安排的隱藏操作。
3、實現一個GestureDetector ,讓它監測onSingleTapUp(MotionEvent),讓用戶可以通過觸摸content手動控制system bar的可見性。簡單的click監聽不是最好的解決方案因為當用戶在屏幕滑動手指都可以觸發。

使用粘性沉浸

當你使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY的flag,在system bar內部區域的滑動會導致其半透明狀態並暫時性的顯示但沒有flag被清除,你的system UI的可見性監聽沒有被觸發。system bar會在一會兒以後再次隱藏或用戶在content交互下。
下圖展示了當使用IMMERSIVE_STICKY的flag時半透明的system bar短暫的顯示然後隱藏
imm-sticky
下面是一個簡單的方法來使用這個flag:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        decorView.setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}
}

如果你喜歡IMMERSIVE_STICKY的flag的自動隱藏行為,但是需要同時顯示你自己的UI控件,使用 IMMERSIVE和Handler.postDelayed()或者其它一些類似的在一會兒之後可以重新進入沉浸模式的方式。

響應UI可見性的變化

要獲得UI可見性變化的通知,需要為你的View注冊View.OnSystemUiVisibilityChangeListener,例如在你的Activity中的onCreate()中:

View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
        (new View.OnSystemUiVisibilityChangeListener() {
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        // Note that system bars will only be "visible" if none of the
        // LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            // TODO: The system bars are visible. Make any desired
            // adjustments to your UI, such as showing the action bar or
            // other navigational controls.
        } else {
            // TODO: The system bars are NOT visible. Make any desired
            // adjustments to your UI, such as hiding the action bar or
            // other navigational controls.
        }
    }
});

通常保持UI與system bar可見性變化的一致性是不錯的實踐。例如,你可以通過這種方式讓action bar和狀態欄保持一致的變化狀態。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved