2013年9月11日 星期三

單元四 隱藏的選單 --- DrawerNavigation

什麼是 DrawerNavigation ? 用 Youtube App 的一張圖來讓大家瞭解.


當你按左上方的 App icon 時, 或者是向左拖拉畫面時,
影藏於左側的選單會跑出來, 這便是所謂的 DrawerNavigation 了.

Android 在 android.supprot.v4 的 SDK 裡提供了 DrawerLayout 以及 ActionBarDrawerToggle,
使得 DrawerNavigation 的製作上有了比較標準的格式.

drawer_layout 畫面

在畫面實作上, 我們用 DrawerLayout 包住主畫面以及左側邊欄畫面,
左側邊欄畫面的 layout_gravity = "left", 這樣 DrawerLayout 就知道它是左側欄了!

drawer_layout.xml
 <android.support.v4.widget.DrawerLayout  
   xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@+id/drawer_layout"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent">  
   <ScrollView  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:paddingLeft="16dp"  
     android:paddingRight="16dp"  
     android:scrollbarStyle="outsideOverlay">  
     <TextView android:id="@+id/content_text"  
          android:layout_width="match_parent"  
          android:layout_height="match_parent"  
          android:text="@string/drawer_layout_summary"  
          android:textAppearance="?android:attr/textAppearanceMedium"/>  
   </ScrollView>  
   <ListView android:id="@+id/left_drawer"  
        android:layout_width="200dp"  
        android:layout_height="match_parent"  
        android:layout_gravity="left"  
        android:background="#ff333333"/>  
 </android.support.v4.widget.DrawerLayout>  

DrawerLayoutActivity 主程式

在程式中調用 DrawerLayout, 須讓 drawer (左側欄) 的出現與 ActionBar 的顯示一致,
因此我們用 ActionBarDrawerToggle 讓 action bar 跟 DrawerLayout 連結在一起,
同時撰寫 DrawerListener 來處理 drawer 的狀態.

另外, 左側欄是一個 ListView, 依照 ListView 的模式處理即可.

DrawerLayoutActivity.class
 @SuppressLint("NewApi")  
 public class DrawerLayoutActivity extends Activity {  
   private DrawerLayout mDrawerLayout;  
   private ListView mDrawer;  
   private TextView mContent;  
   private ActionBarHelper mActionBar;  
   private ActionBarDrawerToggle mDrawerToggle;  
   @Override  
   protected void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.drawer_layout);  
     mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);  
     mDrawer = (ListView) findViewById(R.id.left_drawer);  
     mContent = (TextView) findViewById(R.id.content_text);  
     mDrawerLayout.setDrawerListener(new DemoDrawerListener());  
     mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);  
     mDrawer.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,  
         Shakespeare.TITLES));  
     mDrawer.setOnItemClickListener(new DrawerItemClickListener());  
     mActionBar = createActionBarHelper();  
     mActionBar.init();  
     mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,  
         R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);  
   }  
   private class DrawerItemClickListener implements ListView.OnItemClickListener {  
     @Override  
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {  
       mContent.setText(Shakespeare.DIALOGUE[position]);  
       mActionBar.setTitle(Shakespeare.TITLES[position]);  
       mDrawerLayout.closeDrawer(mDrawer);  
     }  
   }  
   private class DemoDrawerListener implements DrawerLayout.DrawerListener {  
     @Override  
     public void onDrawerOpened(View drawerView) {  
       mDrawerToggle.onDrawerOpened(drawerView);  
       mActionBar.onDrawerOpened();  
     }  
     @Override  
     public void onDrawerClosed(View drawerView) {  
       mDrawerToggle.onDrawerClosed(drawerView);  
       mActionBar.onDrawerClosed();  
     }  
     @Override  
     public void onDrawerSlide(View drawerView, float slideOffset) {  
       mDrawerToggle.onDrawerSlide(drawerView, slideOffset);  
     }  
     @Override  
     public void onDrawerStateChanged(int newState) {  
       mDrawerToggle.onDrawerStateChanged(newState);  
     }  
   }  
   /**  
    * Create a compatible helper that will manipulate the action bar if available.  
    */  
   private ActionBarHelper createActionBarHelper() {  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {  
       return new ActionBarHelperICS();  
     } else {  
       return new ActionBarHelper();  
     }  
   }  
   private class ActionBarHelper {  
     public void init() {}  
     public void onDrawerClosed() {}  
     public void onDrawerOpened() {}  
     public void setTitle(CharSequence title) {}  
   }  
   private class ActionBarHelperICS extends ActionBarHelper {  
     private final ActionBar mActionBar;  
     private CharSequence mDrawerTitle;  
     private CharSequence mTitle;  
     ActionBarHelperICS() {  
       mActionBar = getActionBar();  
     }  
     @Override  
     public void init() {  
       mActionBar.setDisplayHomeAsUpEnabled(true);  
       mActionBar.setHomeButtonEnabled(true);  
       mTitle = mDrawerTitle = getTitle();  
     }  
     @Override  
     public void onDrawerClosed() {  
       super.onDrawerClosed();  
       mActionBar.setTitle(mTitle);  
     }  
     @Override  
     public void onDrawerOpened() {  
       super.onDrawerOpened();  
       mActionBar.setTitle(mDrawerTitle);  
     }  
     @Override  
     public void setTitle(CharSequence title) {  
       mTitle = title;  
     }  
   }  
   @Override  
   protected void onPostCreate(Bundle savedInstanceState) {  
     super.onPostCreate(savedInstanceState);  
     mDrawerToggle.syncState();  
   }  
   @Override  
   public boolean onOptionsItemSelected(MenuItem item) {  
     if (mDrawerToggle.onOptionsItemSelected(item)) {  
       return true;  
     }  
     return super.onOptionsItemSelected(item);  
   }  
   @Override  
   public void onConfigurationChanged(Configuration newConfig) {  
     super.onConfigurationChanged(newConfig);  
     mDrawerToggle.onConfigurationChanged(newConfig);  
   }  
 }  


範例圖片






單元三 拖拉式的畫面轉換 --- ViewPager

在 Android 裡, 拖拉式換頁的元件稱作 ViewPager,
在許多文章式的 App 裡頭可以看到這樣的設計.
這種拖拉式的方式, 其實是利用 ViewPager 以及 Fragment 來達成的.

首先要寫個 adapter 繼承自 FragmentPagerAdapter, 
然後再把 adapter 塞回給 ViewPager 的元件即可.

程式碼不多, 直接看 code.

MainActivity.class
 public class FragmentPagerSupport extends FragmentActivity {  
   static final int NUM_ITEMS = 10;  
   MyAdapter mAdapter;  
   ViewPager mPager;  
   @Override  
   protected void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.fragment_pager);  
     mAdapter = new MyAdapter(getSupportFragmentManager());  
     mPager = (ViewPager)findViewById(R.id.pager);  
     mPager.setAdapter(mAdapter);  
     Button button = (Button)findViewById(R.id.goto_first);  
     button.setOnClickListener(new OnClickListener() {  
       public void onClick(View v) {  
         mPager.setCurrentItem(0);  
       }  
     });  
     button = (Button)findViewById(R.id.goto_last);  
     button.setOnClickListener(new OnClickListener() {  
       public void onClick(View v) {  
         mPager.setCurrentItem(NUM_ITEMS-1);  
       }  
     });  
   }  
   public static class MyAdapter extends FragmentPagerAdapter {  
     public MyAdapter(FragmentManager fm) {  
       super(fm);  
     }  
     @Override  
     public int getCount() {  
       return NUM_ITEMS;  
     }  
     @Override  
     public Fragment getItem(int position) {  
       return ArrayListFragment.newInstance(position);  
     }  
   }  
 }  

fragment_pager.xml
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
     android:orientation="vertical" android:padding="4dip"  
     android:gravity="center_horizontal"  
     android:layout_width="match_parent" android:layout_height="match_parent">  
   <android.support.v4.view.ViewPager  
       android:id="@+id/pager"  
       android:layout_width="match_parent"  
       android:layout_height="0px"  
       android:layout_weight="1">  
   </android.support.v4.view.ViewPager>  
   <LinearLayout android:orientation="horizontal"  
       android:gravity="center" android:measureWithLargestChild="true"  
       android:layout_width="match_parent" android:layout_height="wrap_content"  
       android:layout_weight="0">  
     <Button android:id="@+id/goto_first"  
       android:layout_width="wrap_content" android:layout_height="wrap_content"  
       android:text="@string/first">  
     </Button>  
     <Button android:id="@+id/goto_last"  
       android:layout_width="wrap_content" android:layout_height="wrap_content"  
       android:text="@string/last">  
     </Button>  
   </LinearLayout>  
 </LinearLayout>  

這裡範例除了設 ViewPager 外, 還多設了兩個按鈕以讓 Pager 可以快速跳到最前或最後.

範例圖片





P.S
ArrayListFragment.class 我們沒有實作, 大家開發時依據需要實作 Fragment 即可



單元三 Fragment 的 Tab 應用 --- TabFragment

隨著 Fragment 的興起, TabActivity 這個物件已經不再維護了,
那我們要做類似 TabActivity 的畫面要如何做呢?

方式很簡單,
取得 FragmentTabHost, 再把 Fragment 塞入 Tab 中, 這樣就行了.

程式碼不多, 直接看 code 吧!

MainActivity.class
 public class FragmentTabs extends FragmentActivity {  
   private FragmentTabHost mTabHost;  
   @Override  
   protected void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.fragment_tabs);  
     mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);  
     mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);  
     mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),  
         FragmentStackSupport.CountingFragment.class, null);  
     mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),  
         LoaderCursorSupport.CursorLoaderListFragment.class, null);  
     mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),  
         LoaderCustomSupport.AppListFragment.class, null);  
     mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),  
         LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);  
   }  
 }  

fragment_tabs.xml
 <android.support.v4.app.FragmentTabHost  
   xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@android:id/tabhost"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent">  
   <LinearLayout  
     android:orientation="vertical"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent">  
     <TabWidget  
       android:id="@android:id/tabs"  
       android:orientation="horizontal"  
       android:layout_width="match_parent"  
       android:layout_height="wrap_content"  
       android:layout_weight="0"/>  
     <FrameLayout  
       android:id="@android:id/tabcontent"  
       android:layout_width="0dp"  
       android:layout_height="0dp"  
       android:layout_weight="0"/>  
     <FrameLayout  
       android:id="@+id/realtabcontent"  
       android:layout_width="match_parent"  
       android:layout_height="0dp"  
       android:layout_weight="1"/>  
   </LinearLayout>  
 </android.support.v4.app.FragmentTabHost>  

的 Fragments, 包刮:
CountingFragment.class, CursorLoaderListFragment.class, AppListFragment.class, ThrottleLoaderListFragment.class 我們沒有實作, 大家在開發上根據自己的需求實作即可.

範例圖片






單元三 模組化的UI畫面 --- Fragment

Fragment 可以用來顯示及執行 Activity 的某個需要的動作或畫面.
通常我們先將 Fragment 給模組化, 以方便在 Activity 裡添加.
換句話說, 我們可以用 Activity 來管控(數個) Fragment, 並藉此來決定 UI 畫面的顯示.

Framgent 的緣起

會有這樣的設計, 最早是因為須要處理不同尺寸螢幕的資料顯示, 例如: 平板.


平板的畫面比手機大得多, 因此我們希望可以將畫面切割成兩個部分, 讓左側 Fragment A 的點擊, 直接影響右側 Fragment B 的顯示.

而手機的部分, 因為畫面小, 只能將 Fragment A 與 Fragment B 分別使用不同的 Activity 來處理.

在沒有 Fragment 的情況下, 很可能必須為平板和手機分別寫不同的 Activity,
但有了 Fragment, 只需要做一個畫面就能在不同的 Activity 使用, 減少了重復寫 code 的可能性,
畫面模組化的好處在此時顯現了出來.

以至於在 Android 3.0 之後, Google 將許多元件都以 Fragment 來設計.
例如: Tab, ViewPager ... 等等.

Fragment 的 LifeCycle

Fragment 是依附 Activity 的模組畫面, 因此它的 LifeCycle 本身也與 Activity 息息相關.




看圖示可以知道, Fragment 跟 Activity 的 LifeCycle 很像.
當 Activity 添加 Fragment 時, 會先執行 onAttach(),
如果有預先儲存的 Bundle 資料, 應該在此載入.

在 onCreateView() 時, 要返回 Fragment 的 View.

接著, Framgent 的 onResume(), onStop(), onDestroy() 都是依附著 Activity 執行的. 也就是說,
當 Activity 處於畫面前端, 執行 onResume() 時, Framgent 就執行 onResume() ,
當 Activity 要結束了, 執行 onDestroy() 時, Fragment 就執行 onDestroy().

Fragment 的 LifeCycle 做得比 Activity 更細緻, 應是為了方便更細微的處理.

Fragment 的範例

這一次我們的範例取自 Android Developer 的 FragmentBasics,
用意就是做一個像我們最上面那個圖示一樣的 App,
在大尺寸的平板上, 左側是故事章節選擇的 ListView, 右側則是故事內容的 TextView,

平板畫面:


手機畫面:


Fragment 範例實作

1. 撰寫兩個同檔名 news_articles.xml 的 Layout

 一個放在 res/layout 的資料夾, 一個放在 res/large-large 的資料夾. 放在 res/layout 資料夾下的即是提供手機使用, 在 res/layout-large 下的則提供平板使用

res/layout/news_articles.xml
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@+id/fragment_container"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" />  

res/layout-large/news_articles.xml
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:orientation="horizontal"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent">  
   <fragment android:name="com.example.android.fragments.HeadlinesFragment"  
        android:id="@+id/headlines_fragment"  
        android:layout_weight="1"  
        android:layout_width="0dp"  
        android:layout_height="match_parent" />  
   <fragment android:name="com.example.android.fragments.ArticleFragment"  
        android:id="@+id/article_fragment"  
        android:layout_weight="2"  
        android:layout_width="0dp"  
        android:layout_height="match_parent" />  
 </LinearLayout>  

這裡可以注意到, 在大尺寸的畫面上, 我們塞了兩個 fragment, 並且用 layout_weight 分別派給了這兩個不同等分的大小. (HeadlinesFragment 佔 1/3, ArticleFragment 佔 2/3)

2. 分別撰寫 HeadlinesFragment 以及 ArticleFragment  

HeadLinesFramgnet 跟 ArticleFragment 即是我們要用到的模組畫面, 可以注意到我們在大尺寸的畫面上已經悄悄用到這兩個 Fragment 了. 這種方法叫做靜態載入.

HeadlinesFragment.class
 public class HeadlinesFragment extends ListFragment {  
   OnHeadlineSelectedListener mCallback;  
   public interface OnHeadlineSelectedListener {  
     public void onArticleSelected(int position);  
   }  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     int layout = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ?  
         android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_1;  
     setListAdapter(new ArrayAdapter<String>(getActivity(), layout, Ipsum.Headlines));  
   }  
   @Override  
   public void onStart() {  
     super.onStart();  
     if (getFragmentManager().findFragmentById(R.id.article_fragment) != null) {  
       getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);  
     }  
   }  
   @Override  
   public void onAttach(Activity activity) {  
     super.onAttach(activity);  
     try {  
       mCallback = (OnHeadlineSelectedListener) activity;  
     } catch (ClassCastException e) {  
       throw new ClassCastException(activity.toString()  
           + " must implement OnHeadlineSelectedListener");  
     }  
   }  
   @Override  
   public void onListItemClick(ListView l, View v, int position, long id) {  
     mCallback.onArticleSelected(position);      
     getListView().setItemChecked(position, true);  
   }  
 }  

在 HeadlinesFragment.class, 我們寫了一個 onHeadlineSelectedListener 的 interface, 裡頭有 onArticleSelected() , 代表 implement 這個 onHeadlineSelectedListener 的 Activity 要實作 onArticleSelected(). 藉此, 便可以在 onAttach() 的時候取得這個 Listener 物件, 做為 Fragment 跟 Activity 溝通使用.

ArticleFragment.class
 public class ArticleFragment extends Fragment {  
   final static String ARG_POSITION = "position";  
   int mCurrentPosition = -1;  
   @Override  
   public View onCreateView(LayoutInflater inflater, ViewGroup container,   
     Bundle savedInstanceState) {  
     if (savedInstanceState != null) {  
       mCurrentPosition = savedInstanceState.getInt(ARG_POSITION);  
     }  
     return inflater.inflate(R.layout.article_view, container, false);  
   }  
   @Override  
   public void onStart() {  
     super.onStart();  
     Bundle args = getArguments();  
     if (args != null) {  
       updateArticleView(args.getInt(ARG_POSITION));  
     } else if (mCurrentPosition != -1) {  
       updateArticleView(mCurrentPosition);  
     }  
   }  
   public void updateArticleView(int position) {  
     TextView article = (TextView) getActivity().findViewById(R.id.article);  
     article.setText(Ipsum.Articles[position]);  
     mCurrentPosition = position;  
   }  
   @Override  
   public void onSaveInstanceState(Bundle outState) {  
     super.onSaveInstanceState(outState);  
     outState.putInt(ARG_POSITION, mCurrentPosition);  
   }  
 }  

在ArticleFragment.class 的任務很簡單, 就是要把資料顯示在 article_view.xml 上.

3. 在 MainActivity 裡調用.

在MainActivity 裡, 我們必須大尺寸的雙畫面, 跟小尺寸的單畫面分別作不同的處置.
那要如何知道目前是大尺寸或小尺寸呢? 可以用有沒有 R.id.fragment_container 來判別.
(因為之前有說過, 裝置會自動調用 res/layout for 小尺寸, res/layout-large for 大尺寸)

如果有 R.id.fragment_container => 小尺寸, 沒有 R.id.fragment_container => 大尺寸.

MainActivity.class
 public class MainActivity extends FragmentActivity   
     implements HeadlinesFragment.OnHeadlineSelectedListener {  
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.news_articles);  
     if (findViewById(R.id.fragment_container) != null) {  
       if (savedInstanceState != null) {  
         return;  
       }  
       HeadlinesFragment firstFragment = new HeadlinesFragment();  
       firstFragment.setArguments(getIntent().getExtras());  
       getSupportFragmentManager().beginTransaction()  
           .add(R.id.fragment_container, firstFragment).commit();  
     }  
   }  
   public void onArticleSelected(int position) {  
     ArticleFragment articleFrag = (ArticleFragment)  
         getSupportFragmentManager().findFragmentById(R.id.article_fragment);  
     if (articleFrag != null) {  
       articleFrag.updateArticleView(position);  
     } else {  
       ArticleFragment newFragment = new ArticleFragment();  
       Bundle args = new Bundle();  
       args.putInt(ArticleFragment.ARG_POSITION, position);  
       newFragment.setArguments(args);  
       FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();  
       transaction.replace(R.id.fragment_container, newFragment);  
       transaction.addToBackStack(null);  
       transaction.commit();  
     }  
   }  
 }  

同理, 如果 article_fragment 存在 => 大尺寸, 如果 article_fragment 不存在 => 小尺寸.
大尺寸當 HeadlinesFragment 被點擊時要 update ArticleFragment 的內容,
小尺寸當 HeadlinesFragment 被點擊時要產生新的 ArticleFragment, 並更新畫面.

原始碼連結





2013年9月10日 星期二

單元一: 優化APP (Making APPs Beautiful)

關於要如何優化APP, Google 的工程師 Ankur Kotwal 做了一系列的 Youtube 教學影片,
我將其做點整理, 剛好可以拿來當做進階單元的一個開端.

這一系列影片共有五集, 從最初的 App 樣式一直修改至最後比較現代化的版本樣式( ActionBar, Theme, Fragment ... 等等), 也提供了支援 Android 2.3 之前的版本.

除此之外, 影片包含了許多 Google 建議我們的 Design Guide Line, 方便我們在 App  開發時, 更能遵從開發的一致性, 縮短開發設計時間, 十分值得好好學習.

Part 1. Modernize

在 Android 3.0 ( SDK version 10)之後, Google 加入了許多設計元素, 使得操作上更直覺, 畫面也更漂亮. 這段影片講解如何使用 ActionBar, 來使得 App 更有設計感, 同時也 ActionBar 的客制化連結.




Part  2. Spacing and Typography

以 ListView, DetailView 這兩個畫面的邊界設定和字型為例, 說明如何讓畫面看起來更有設計感.



Part 3. Tablets

平板跟手機不一樣的地方在於平板的畫面遠大於手機的畫面, 因此如果將手機畫面原原本本地搬到平板上, 顯然就不是那麼地實用. 因此, 利用 Fragment 將平板的畫面切割給不同的資料顯示, 以求使用者有更好地使用體驗.



Part 4. Performance Tuning

藉由使用 AsyncTask 以及 Cache 的方式來減低主線程不必要的工作量, 一來可以減低使用時卡卡的感覺, 二來可以縮短顯示時間.



Part 5 Backwards Compatibility and Modernization

Android 新舊版本要有更好地相容,
在 ActionBar 的部分可以使用 ActionBarCompact 或 ActionBarSherlock,
另外, 適當地使用 ViewPager, Fragment, Navigation Drawer ... 等,
都是很棒的方式.



原始碼連結

找到了 Ankur 在 GitHub 上提供的原始碼, 相信對大家在實作上會有所幫助.




2013年9月7日 星期六

單元二: 手機的資料儲存 --- SQLite + ContentProvider + Loader

上一篇, 說明了 SQLite 直接調用的方法,

我們在這一篇裡, 想再說明另一種調用方式: SQLite + ContentProvider + Loader.
這種方式是透過 Loader 去調用 ContentProvider,
ContentProvider 再呼叫 SQLite 執行增刪改查的動作.


使用 Loader 有兩個好處,
一. 會異步更新資料 (Asynchronous Loading Data), 也就是不會佔到 Main Thread 的資源,
二. 可以監視 Data 的改變, 當 Data 改變時會通知 Main Thread.

在 Android Developer 的文件裡, 是建議我們使用 Loader 的.

SQLite + ContentProvider + Loader 的使用

使用上我們一樣分成幾個步驟:
1. 寫 ProductTable, 在這個 class 裡存放我們的資料檔名稱, 以及 provider 需要的路徑
2. 寫 ProductDBHelper ( 繼承自 SQLiteOpenHelper )
3. 寫 ProductProvider ( 繼承自 ContentProvider )
4. 在主程式裡調用
5. 在 AndroidManifest.xml 裡註冊 provider

我們的目標一樣是做上一篇的那張表, 並顯示在  ListView 上.

1. 寫 ProductTable

這個 ProductTable 繼承自 BaseColumns,
BaseColumns 包含了 _ID, _COUNT 兩個常數,
所以我們在設置常數的時候, 可以直接調用.

這裡我們要設置檔案的 Table 名稱, Table Column 的名稱,
以及 Provider 需要的 uri 路徑.

ProdcutTable.class
 public final class ProductTable implements BaseColumns {  
   // This class cannot be instantiated  
   private ProductTable() {}  
   /**  
    * The table name offered by this provider  
    */  
   public static final String TABLE_NAME = "product";  
   /**  
    * The content:// style URL for this table  
    */  
   public static final Uri CONTENT_URI = Uri.parse("content://" + MainActivity.AUTHORITY + "/"+TABLE_NAME);  
   /**  
    * The content URI base for a single row of data. Callers must  
    * append a numeric row id to this Uri to retrieve a row  
    */  
   public static final Uri CONTENT_ID_URI_BASE  
       = Uri.parse("content://" + MainActivity.AUTHORITY + "/"+TABLE_NAME+"/");  
   /**  
    * The MIME type of {@link #CONTENT_URI}.  
    */  
   public static final String CONTENT_TYPE  
       = "vnd.android.cursor.dir/vnd.example.api-demos-throttle";  
   /**  
    * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row.  
    */  
   public static final String CONTENT_ITEM_TYPE  
       = "vnd.android.cursor.item/vnd.example.api-demos-throttle";  
   /**  
    * The default sort order for this table  
    */  
   public static final String DEFAULT_SORT_ORDER = _ID+" COLLATE LOCALIZED ASC";  
   /**  
    * Column name for the single column holding our data.  
    * <P>Type: TEXT</P>  
    */  
   public static final String COLUMN_NAME_DATA = "product_name";  
 }  

2. 寫 ProductDBHelper

ProductDBHelper 繼承自 SQLiteOpenHelper, 用來執行 SQLite 的指令.

ProductDBHelper.class
 public class ProductDBHelper extends SQLiteOpenHelper {  
   private static final String DATABASE_NAME = "productDB2.db";  
   private static final int DATABASE_VERSION = 2;  
   ProductDBHelper(Context context) {  
     // calls the super constructor, requesting the default cursor factory.  
     super(context, DATABASE_NAME, null, DATABASE_VERSION);  
   }  
   @Override  
   public void onCreate(SQLiteDatabase db) {  
     db.execSQL("CREATE TABLE " + ProductTable.TABLE_NAME + " ("  
         + ProductTable._ID + " INTEGER PRIMARY KEY,"  
         + ProductTable.COLUMN_NAME_DATA + " TEXT"  
         + ");");  
   }  
   @Override  
   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
     // Kills the table and existing data  
     db.execSQL("DROP TABLE IF EXISTS notes");  
     // Recreates the database with a new version  
     onCreate(db);  
   }  
 }  

3. 寫 ProductProvider

我們透過 ProductProvider 來調用 ProductDBHelper,
這樣之後在主程式裡使用的時候, 就只需要管理 ProductProvider 這個接口就好.

ProductProvider.class
 public class ProductProvider extends ContentProvider {  
   // A projection map used to select columns from the database  
   private final HashMap<String, String> mNotesProjectionMap;  
   // Uri matcher to decode incoming URIs.  
   private final UriMatcher mUriMatcher;  
   // The incoming URI matches the main table URI pattern  
   private static final int MAIN = 1;  
   // The incoming URI matches the main table row ID URI pattern  
   private static final int MAIN_ID = 2;  
   // Handle to a new DatabaseHelper.  
   private ProductDBHelper mOpenHelper;  
   /**  
    * Global provider initialization.  
    */  
   public ProductProvider() {  
     // Create and initialize URI matcher.  
     mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);  
     mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.TABLE_NAME, MAIN);  
     mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.TABLE_NAME + "/#", MAIN_ID);  
     // Create and initialize projection map for all columns. This is  
     // simply an identity mapping.  
     mNotesProjectionMap = new HashMap<String, String>();  
     mNotesProjectionMap.put(ProductTable._ID, ProductTable._ID);  
     mNotesProjectionMap.put(ProductTable.COLUMN_NAME_DATA, ProductTable.COLUMN_NAME_DATA);  
   }  
   /**  
    * Perform provider creation.  
    */  
   @Override  
   public boolean onCreate() {  
     mOpenHelper = new ProductDBHelper(getContext());  
     // Assumes that any failures will be reported by a thrown exception.  
     return true;  
   }  
   /**  
    * Handle incoming queries.  
    */  
   @Override  
   public Cursor query(Uri uri, String[] projection, String selection,  
       String[] selectionArgs, String sortOrder) {  
     // Constructs a new query builder and sets its table name  
     SQLiteQueryBuilder qb = new SQLiteQueryBuilder();  
     qb.setTables(ProductTable.TABLE_NAME);  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If the incoming URI is for main table.  
         qb.setProjectionMap(mNotesProjectionMap);  
         break;  
       case MAIN_ID:  
         // The incoming URI is for a single row.  
         qb.setProjectionMap(mNotesProjectionMap);  
         qb.appendWhere(ProductTable._ID + "=?");  
         selectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,  
             new String[] { uri.getLastPathSegment() });  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     if (TextUtils.isEmpty(sortOrder)) {  
       sortOrder = ProductTable.DEFAULT_SORT_ORDER;  
     }  
     SQLiteDatabase db = mOpenHelper.getReadableDatabase();  
     Cursor c = qb.query(db, projection, selection, selectionArgs,  
         null /* no group */, null /* no filter */, sortOrder);  
     c.setNotificationUri(getContext().getContentResolver(), uri);  
     return c;  
   }  
   /**  
    * Return the MIME type for an known URI in the provider.  
    */  
   @Override  
   public String getType(Uri uri) {  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         return ProductTable.CONTENT_TYPE;  
       case MAIN_ID:  
         return ProductTable.CONTENT_ITEM_TYPE;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
   }  
   /**  
    * Handler inserting new data.  
    */  
   @Override  
   public Uri insert(Uri uri, ContentValues initialValues) {  
     if (mUriMatcher.match(uri) != MAIN) {  
       // Can only insert into to main URI.  
       throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     ContentValues values;  
     if (initialValues != null) {  
       values = new ContentValues(initialValues);  
     } else {  
       values = new ContentValues();  
     }  
     // 如果傳進來的 initialValues 是 null, 讓資料為 ""  
     if (values.containsKey(ProductTable.COLUMN_NAME_DATA) == false) {  
       values.put(ProductTable.COLUMN_NAME_DATA, "");  
     }  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     long rowId = db.insert(ProductTable.TABLE_NAME, null, values);  
     // If the insert succeeded, the row ID exists.  
     if (rowId > 0) {  
       Uri noteUri = ContentUris.withAppendedId(ProductTable.CONTENT_ID_URI_BASE, rowId);  
       getContext().getContentResolver().notifyChange(noteUri, null);  
       return noteUri;  
     }  
     throw new SQLException("Failed to insert row into " + uri);  
   }  
   /**  
    * Handle deleting data.  
    */  
   @Override  
   public int delete(Uri uri, String where, String[] whereArgs) {  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     String finalWhere;  
     int count;  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If URI is main table, delete uses incoming where clause and args.  
         count = db.delete(ProductTable.TABLE_NAME, where, whereArgs);  
         break;  
         // If the incoming URI matches a single note ID, does the delete based on the  
         // incoming data, but modifies the where clause to restrict it to the  
         // particular note ID.  
       case MAIN_ID:  
         // If URI is for a particular row ID, delete is based on incoming  
         // data but modified to restrict to the given ID.  
         finalWhere = DatabaseUtilsCompat.concatenateWhere(  
                   ProductTable._ID + " = " + ContentUris.parseId(uri), where);  
         count = db.delete(ProductTable.TABLE_NAME, finalWhere, whereArgs);  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     getContext().getContentResolver().notifyChange(uri, null);  
     return count;  
   }  
   /**  
    * Handle updating data.  
    */  
   @Override  
   public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     int count;  
     String finalWhere;  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If URI is main table, update uses incoming where clause and args.  
         count = db.update(ProductTable.TABLE_NAME, values, where, whereArgs);  
         break;  
       case MAIN_ID:  
         // If URI is for a particular row ID, update is based on incoming  
         // data but modified to restrict to the given ID.  
         finalWhere = DatabaseUtilsCompat.concatenateWhere(  
                   ProductTable._ID + " = " + ContentUris.parseId(uri), where);  
         count = db.update(ProductTable.TABLE_NAME, values, finalWhere, whereArgs);  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     getContext().getContentResolver().notifyChange(uri, null);  
     return count;  
   }  
 }  

這裡用了一個技巧是使用 UriMatcher 透過路徑來分辨是要對 table 或者是 row 進行刪,查的動作, 也是挺實用的.

4. 在主程式裡調用

在 MainActivity 要使用 LoaderManager 必須繼承自 FragmentActivity,
並且 implements LoaderManager.LoaderCallbacks<Cursor>,
這樣就能有 Loader  的回呼函數: onCreateLoader(), onLoadFinished(), onLoaderReset().

實作上, 我們取得 ContentResolver (可以用來執行 ContentProvider 裡的方法),
接著存入幾筆資料, 再利用 Loader 讀回來.

MainActivity.class
 public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>{  
      /**  
    * The authority we use to get to our sample provider.  
    */  
   public static final String AUTHORITY = "com.example.learnsqliteusingloader";  
   private ListView mainList;  
  // This is the Adapter being used to display the list's data.  
   private SimpleCursorAdapter mAdapter;  
      @SuppressLint("NewApi")  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_main);  
           mainList = (ListView) findViewById (R.id.main_list);  
           // Create an empty adapter we will use to display the loaded data.  
     mAdapter = new SimpleCursorAdapter(this,  
         R.layout.item_list, null,  
         new String[] { ProductTable.COLUMN_NAME_DATA },  
         new int[] { R.id.text_list }, 0);  
     mainList.setAdapter(mAdapter);  
           // 寫入資料  
     ContentResolver cr = getContentResolver();  
     for (int i=0;i<11;i++){  
          ContentValues values = new ContentValues();  
          values.put(ProductTable.COLUMN_NAME_DATA, "cup_"+ Integer.toString(i));  
          cr.insert(ProductTable.CONTENT_URI, values);  
     }  
     // Prepare the loader. Either re-connect with an existing one,  
     // or start a new one.  
     getSupportLoaderManager().initLoader(0, null, this);  
      }  
      @Override  
      public boolean onCreateOptionsMenu(Menu menu) {  
           // Inflate the menu; this adds items to the action bar if it is present.  
           getMenuInflater().inflate(R.menu.main, menu);  
           return true;  
      }  
      static final String[] PROJECTION = new String[] {  
           ProductTable._ID,  
           ProductTable.COLUMN_NAME_DATA,  
   };  
      @Override  
      public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {  
           CursorLoader cl = new CursorLoader(this, ProductTable.CONTENT_URI,  
         PROJECTION, null, null, null);  
     return cl;  
      }  
      @Override  
      public void onLoadFinished(Loader<Cursor> loader, Cursor data) {  
           mAdapter.swapCursor(data);  
      }  
      @Override  
      public void onLoaderReset(Loader<Cursor> loader) {  
           mAdapter.swapCursor(null);  
      }  
 }  

5. 在 AndroidManifest.xml 裡註冊 provider

最後要在 AndroidManifest.xml 裡註冊 provider,
特別要注意 authority 的路徑要跟在 MainActivity.class 設的 AUTHORITY 一樣.

因為我們在 ProductTable.class 裡設的許多路徑, 也是跟著這個 AUTHORITY 設的.
必須要是這個路徑才能調用我們的 ProductProvider

AndroidManifest.xml
 <provider android:name=".ProductProvider"  
          android:authorities="com.example.learnsqliteusingloader" />  


範例圖: (原始碼連結)






2013年9月6日 星期五

單元二: 手機的資料儲存 --- SQLite

Android SQLite 是 Android 手機上管理資料存取的系統,  它可以執行增(insert), 刪(delete), 改(update), 查(read)的動作, 特點是生成的檔案很輕量, 也支持標準的 SQL 語句.

要瞭解 SQLite 的使用, 必須先認識一下資料庫的格式:

資料庫格式

資料庫一般可以想成是一張表, 整張表會有個名稱叫 table_name,
表直的部分稱作 Column, 橫的叫 Row,
這裡的 product_id, product_name 都是 column_name,
而每個 Row 都是一組 Data,
例如: product_id = 1, product_name = cup_1 就是我們這張表的第一組 Data 了.



SQLite 的 Type 只有 TEXT, INTEGER, REAL ( similar to double in java) 三種. 使用上要稍注意.

SQLite 的使用

SQLite 使用上可以分三個步驟:
1. 建立資料的物件 Product
2. 建立繼承自 SQLiteOpenHelper 的 DatabasHelper
3. 在主程式內調用

我們就以一開始介紹的那張表為例, 來講解這三個步驟.

1. 建立資料的物件 Product

Product.class
 public class Product {  
      private int id;  
      private String name;  
      public Product() {  
      }  
      public Product(int id, String productname) {  
           this.id = id;  
           this.name = productname;  
      }  
      public Product(String productname) {  
           this.name = productname;  
      }  
      public void setID(int id) {  
           this.id = id;  
      }  
      public int getID() {  
           return this.id;  
      }  
      public void setProductName(String productname) {  
           this.name = productname;  
      }  
      public String getProductName() {  
           return this.name;  
      }  
 }  

2. 建立繼承自 SQLiteOpenHelper 的 DatabasHelper

SQLiteOpenHelper 是 Android 幫我們準備好的元件,
我們要繼承它, 並實作 onCreate(), 以及 onUpgrade().
OnCreate() 用來創建一張新的表,
onUpgrade() 則會先檢查這張表的 version, 如果有新 version 就把舊的丟掉, 呼叫 onCreate() 做張新的.

ProductDBHelper.class
      @Override  
      public void onCreate(SQLiteDatabase db) {  
           String CREATE_PRODUCTS_TABLE = "CREATE TABLE " +  
             TABLE_PRODUCTS + "("  
             + COLUMN_ID + " INTEGER PRIMARY KEY," + COLUMN_PRODUCTNAME   
             + " TEXT" + ")";  
         db.execSQL(CREATE_PRODUCTS_TABLE);  
      }  
      @Override  
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
           db.execSQL("DROP TABLE IF EXISTS " + TABLE_PRODUCTS);  
         onCreate(db);  
      }  

同時, 我們也把增.刪.查的方法寫在 ProductDBHelper.class 裡, 方便之後使用

ProductDBHelper.class
      // 增加單筆資料  
      public void addProduct(Product product) {  
          ContentValues values = new ContentValues();  
          values.put(COLUMN_PRODUCTNAME, product.getProductName());  
          SQLiteDatabase db = this.getWritableDatabase();  
          db.insert(TABLE_PRODUCTS, null, values);  
          db.close();  
      }  
      // 找所有資料  
      public ArrayList<Product> getAllProduct(){       
           ArrayList<Product> products = new ArrayList<Product>();  
           String query = "Select * FROM " + TABLE_PRODUCTS + " ORDER BY _ID ASC";  
           SQLiteDatabase db = this.getWritableDatabase();  
           Cursor cursor = db.rawQuery(query, null);  
           int rows_num = cursor.getCount();  
            if(rows_num != 0) {  
            cursor.moveToFirst();    
            for(int i=0; i<rows_num; i++) {  
             Product product = new Product();  
             product.setID(Integer.parseInt(cursor.getString(0)));  
             product.setProductName(cursor.getString(1));  
             products.add(product);  
             cursor.moveToNext();  
            }  
            }  
            cursor.close();  
           return products;  
      }  
      // 找單筆資料  
      public Product findProduct(String productname) {  
           String query = "Select * FROM " + TABLE_PRODUCTS + " WHERE " + COLUMN_PRODUCTNAME + " = \"" + productname + "\"";  
           SQLiteDatabase db = this.getWritableDatabase();  
           Cursor cursor = db.rawQuery(query, null);  
           Product product = new Product();  
           if (cursor.moveToFirst()) {  
                cursor.moveToFirst();  
                product.setID(Integer.parseInt(cursor.getString(0)));  
                product.setProductName(cursor.getString(1));  
                cursor.close();  
           } else {  
                product = null;  
           }  
          db.close();  
           return product;  
      }  
      // 刪除資料  
      public boolean deleteProduct(String productname) {  
           boolean result = false;  
           String query = "Select * FROM " + TABLE_PRODUCTS + " WHERE " + COLUMN_PRODUCTNAME + " = \"" + productname + "\"";  
           SQLiteDatabase db = this.getWritableDatabase();  
           Cursor cursor = db.rawQuery(query, null);  
           Product product = new Product();  
           if (cursor.moveToFirst()) {  
                product.setID(Integer.parseInt(cursor.getString(0)));  
                db.delete(TABLE_PRODUCTS, COLUMN_ID + " = ?",  
                 new String[] { String.valueOf(product.getID()) });  
                cursor.close();  
                result = true;  
           }  
          db.close();  
           return result;  
      }  

3. 在主程式內調用

在這個程式, 我們先產生 product 的資料, 將其寫入資料庫裡,
然後再讀出來設給 ListView

MainActivity.class
          // 寫入資料  
           ProductDBHelper dbHandler = new ProductDBHelper(this, null, null, 1);  
           for (int i = 0 ; i< 11 ; i++){       
                Product newProduct = new Product("cup_"+ Integer.toString(i));  
                dbHandler.addProduct(newProduct);  
           }  
           // 讀取資料  
           mProducts = dbHandler.getAllProduct();  
           ListAdapter adapter = new ListAdapter(this, mProducts);  
           mainList.setAdapter(adapter);  

當然我們還必須實作 ListAdapter. 不過這不是這篇的重點.
就請直接看程式碼吧.

P.S 如果對於 ListAdapter 不大瞭解, 可以看 ListView 的教學.

範例圖: (原始碼連結)




2013年9月4日 星期三

單元一: Android 的影音播放 --- MediaPlayer

隨著入門課程告一段落, 我們接著講解的各個單元會比較沒怎麼彼此相關性.
大家可以挑自己有興趣, 有需要的部分學習就好~~
我就以 MediaPlayer 當做這個基礎單元的起頭吧!

認識 MediaPlayer

要妥善處理我們裝置的影音播放, 有兩個元件要認識
一.  MediaPlayer
MediaPlayer 是播放影音主要的 API (Application Programming Interface, 應用程序接口), 我們透個這個 API 管理影音的播放或停止等動作.
二. AudioManager
AudioManager 是用來管理手機影音資源的, 包括聲音的輸出, 以及目前應該由哪個程式取得播放權..等等.

概述

MediaPlayer 的處理是 state_based 的, 也就是跟它目前所處的狀態有關.
先看一下底下這張圖表. 藍色就表示所處的狀態(state), 箭頭旁邊則是可以呼叫的方法(method).


當我們的新產生一個 MediaPlayer 時, 會先處於 idle 的狀態,
接著執行 setDataResource() 指定資料來源後, 就會跑到 initialize 的狀態,
執行 prepare() 時, 它(MediaPlayer)會開始去截取資料並解碼, 等解碼完畢就會進入 prepared.

這邊需要注意的是, 截取資料並解碼是一個比較耗時的過程, 應該要另外開啟副線程(Thread)來執行. 或者是使用 prepareAsync() 這個 Android 幫我們準備好的方法.
* prepareAsync() 本身即是開啓副線程來處理資料.

等到 MediaPlayer 進入 prepared 的狀態後, 即可執行 start() 開始播放,
接著播放中亦或播放結束會視情況不同進入 Paused, Stopped, PlaybackCompleted 的狀態.

最重要的是必須避免在不適當的狀態呼叫不適當的method. 這樣會造成出錯.
(例如還沒 setDataSource() 就 prepare().)

MediaPlayer 的範例 --- 以 RandomMediaPlayer 為例 (原始碼連結)

RandomMediaPlayer 是 Android Developer 上的一個範例.
它包含了幾個部分:
1. 截取手機上可以播放的音樂資料
2. 設置 service, 讓音樂可以在背景播放
3. 設置 Remote Control, 讓音樂播放時在 Screen Lock 的情況下也能操控.
4. 處理 AudioFocus 的問題
這個範例相當精彩, 我們就將上面各部分切割講解.

截取手機上可以播放的音樂資料

利用 MusicRetriever 來取得手機內可以播放的資源.

首先, 取得 SD 卡上面音樂資料的位置
MusicRetriever.class
 Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;  

接著利用這個位置取得可以 query 資料用的 cursor
MusicRetriever.class
 Cursor cur = mContentResolver.query(uri, null,  
         MediaStore.Audio.Media.IS_MUSIC + " = 1", null, null);  

這樣就可以取得手機上的音樂資料了,
範例裡還定義了 Item 這個物件, 並且足一取出 cursor 的資料塞到 List<Item> 裡,
方便在 service 呼叫時使用.

設置 service, 讓音樂可以在背景播放

在 service 的入門課程有提到過, 我們是利用 intent 來開啓 service 的.
這個範例裡也是一漾, 先設置了播放, 停止, 回播, 快轉等按鈕,
當我們點擊按鈕時, 主程式就會傳 intent 開啟 service.
MainActivity.class
 public void onClick(View target) {  
     // Send the correct intent to the MusicService, according to the button that was clicked  
     if (target == mPlayButton)  
       startService(new Intent(MusicService.ACTION_PLAY));  
     else if (target == mPauseButton)  
       startService(new Intent(MusicService.ACTION_PAUSE));  
     else if (target == mSkipButton)  
       startService(new Intent(MusicService.ACTION_SKIP));  
     else if (target == mRewindButton)  
       startService(new Intent(MusicService.ACTION_REWIND));  
     else if (target == mStopButton)  
       startService(new Intent(MusicService.ACTION_STOP));  
     else if (target == mEjectButton) {  
       showUrlDialog();  
     }  
   }  


MusicService 收到後會依據 Action 的不同執行不同的動作
MusicService.class
 if (action.equals(ACTION_TOGGLE_PLAYBACK)) processTogglePlaybackRequest();  
     else if (action.equals(ACTION_PLAY)) processPlayRequest();  
     else if (action.equals(ACTION_PAUSE)) processPauseRequest();  
     else if (action.equals(ACTION_SKIP)) processSkipRequest();  
     else if (action.equals(ACTION_STOP)) processStopRequest();  
     else if (action.equals(ACTION_REWIND)) processRewindRequest();  
     else if (action.equals(ACTION_URL)) processAddRequest(intent);  

如果是 action_play 即執行 processPlayRequest(),
這時便要透過 MusicRetriever 取得 Item, 並設置 MediaPlayer 的 Source, 並播放.
MusicService.class
 450 playingItem = mRetriever.getRandomItem();  
 462 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);  
 463 mPlayer.setDataSource(getApplicationContext(), playingItem.getURI());  
 517 mPlayer.prepareAsync();  
 400 mPlayer.start();  

關鍵的程式順序是這幾句, 它們分別被封裝在不同的 Method 裡,
我將它們出現的行數寫在左邊.

設置 Remote Control

這個音樂的 Remote Control 是Android 4.0 以上才有的功能.
這部分不實作對程式不會有什麼影響, 但是還滿有趣的, 我們也說明一下.



在 Screen Lock 的情況下出現的 Button 叫 MediaButton, 它是 Android 幫我們實作的.
我們要透過 MediaButtonHelper 註冊.
MusicService.class
 MediaButtonHelper.registerMediaButtonEventReceiverCompat(  
           mAudioManager, mMediaButtonReceiverComponent);  

接著, 取得 Media 的 RemoteControlComponent,
然後利用 RemoteControlHelper 註冊來讓手機知道.
MusicService.class
 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);  
 intent.setComponent(mMediaButtonReceiverComponent);  
 mRemoteControlClientCompat = new RemoteControlClientCompat(  
             PendingIntent.getBroadcast(this, 0 , intent , 0));  
 RemoteControlHelper.registerRemoteControlClient(mAudioManager,  
             mRemoteControlClientCompat);  

處理 AudioFocus 的問題

在處理影音的時候,要注意何時應該取得 AudioFocus, 何時放棄 AudioFocus,
只有取得 AudioFocus 才能讓喇叭發出聲音,
所以我們在播放前, 要先取得 AudioFocus,
如果播放中有電話打來, 則要放棄 AudioFocus.

舉我們在執行 porcessPlayRequest() 的時候為例, 需要先要求 AudioFocus,
MusicService.class
 void processPlayRequest() {  
     ...  
     tryToGetAudioFocus();  
     ...  
 }  
MusicService.class
 void tryToGetAudioFocus() {  
     if (mAudioFocus != AudioFocus.Focused && mAudioFocusHelper != null  
             && mAudioFocusHelper.requestFocus())  
       mAudioFocus = AudioFocus.Focused;  
 }  

真正幫助我們取得 Focus 的是 AudioFocusHelper 這個元件,
這邊的 AudioFocus 是我們自定義的 enum, 是依據 Android 的 Focus 形態而定的,
Duck  的意思是可以小聲播放,
MusicService.class
 enum AudioFocus {  
     NoFocusNoDuck,  // we don't have audio focus, and can't duck  
     NoFocusCanDuck,  // we don't have focus, but can play at a low volume ("ducking")  
     Focused      // we have full audio focus  
 }  

其他的 processStopRequest(), processSkipRequest(), ...等也都要注意 AudioFocus的問題,
我們就不細述了.

範例圖片(原始碼連結)







2013年9月3日 星期二

第十課: APP的資料分享 --- ContentProvider


在 Android 的架構裡, 每個 App 就像是獨立的個體, 彼此不互相影響,
這樣做的好處是可以避免正常的程式被不正常的程式所影響,
但程式間的資料分享就成了一個問題.
ContentProvider 就是為了解決這個 App 間資料分享的問題而生的.

ContentProvider 的功用

ContentProvider 的目的是提供一個 App 間溝通的管道, 
所以它其實有兩個層面:
1. 提供資料給其他的 App 使用
2. 取得其他 App 提供的資料

在這邊不講解如何提供資料給其他 App 使用, 有需要的話可以看看這篇.
倒是我們比較可能需要取得其他 App 提供的資料, 下邊做個整理.

Android 提供的資源

Android 本身各個App提供的資源有:
鬧鐘 (AlarmClock),
通訊錄 (Contact),
行事曆 (Calendar),
相簿 (Photo),
影音列表 (MediaStore) ...等

如此一來, 開發者便能透過 Provider 來取得手機上的資料.

ContentProvider 取資料的範例 ---  以 Contacts 為例 (原始碼連結)

一.  註冊 permission, 取得Contacts 的使用權

 <uses-permission android:name="android.permission.GET_ACCOUNTS" />  
 <uses-permission android:name="android.permission.READ_CONTACTS" />  
 <uses-permission android:name="android.permission.WRITE_CONTACTS" />  

二. 透過 Provider 取得 cursor
cursor 是Android 裡用來取得資料的物件,  取得 cursor 後就能查找我們要的任一筆資料.
這裡的 getContacts() 是範例寫的一個方法,
projection 這個 String[] 要放我們想要的欄位名稱,
selection, selectionArgs 可以給 query 資料的條件, 設 null 就表示全選.

 private Cursor getContacts()  
   {  
     Uri uri = ContactsContract.Contacts.CONTENT_URI;  
     String[] projection = new String[] {  
         ContactsContract.Contacts._ID,  
         ContactsContract.Contacts.DISPLAY_NAME  
     };  
     String selection = null;  
     String[] selectionArgs = null;  
     String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";  
     return managedQuery(uri, projection, selection, selectionArgs, sortOrder);  
   }  

三. 使用SimpleCursorAdapter 取得 Adapter

這邊的 fields 是想取得資料欄位的名稱, layout.contact_entry 則是自定義的 layout.
取得 adapter 後, 再設給 ListView 就行了.

 String[] fields = new String[] {  
      ContactsContract.Data.DISPLAY_NAME  
 };  
 SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.contact_entry, cursor,  
         fields, new int[] {R.id.contactEntryText});  
 mList.setAdapter(adapter);  


範例(原始碼連結)

範例是取得通訊錄的內容, 做一個通訊錄的ListView



影片教學




2013年9月2日 星期一

第九課: 廣播接收器 --- BroadCastReceiver

當手機接收到外來的訊息,
例如: 收到簡訊, 接到來電, 或者是 App 本身的廣播...等.
我們的應用程式 App 可以透過 BroadCastReceiver 接收並做出回應,

BroadCastReceiver 的使用方式

1. 寫一個繼承自 BroadCastReceiver 的 Class, 處理 onReceive() 事件.
2. 在 AndroidManifest 裡註冊這個 BroadCastReceiver Class

這樣一但接收到系統的廣播或者是自己應用程式的廣播時,
我們的 BroadCastReceiver 就會執行 onReceive() 的程式碼.

BroadCastReceiver 的生命週期

BroadCastReceiver 的生命週期非常短:

onReceive() --> 結束

需要注意的是, onReceive 的程式碼執行不能超過五秒,
否則會彈出 "應用程式沒有回應" 的 Dialog.

因此, 我們應該在 onReceive 時, 發送 Notification 提示使用者,
或者直接把長時間的運算透過 intent 丟給 Service.

BroadCast 廣播的發送

App 內廣播的發送是透過 intent 以及 sendBroadCast(intent) 來發送的

 Intent intent = new Intent();  
 intent.setAction(ACTION_SHOW_DIALOG);  
 sendBroadcast(intent);  

BroadCast 的範例 (程式碼連結)

我們範例做一個按鈕, 點擊了之後發送 BroadCast, 然後利用 BroadCastReceiver 接收回來並彈出接收到 BroadCast 的 Dialog 確認框.



影片教學