1. 程式人生 > >Android Settings 快速搜尋

Android Settings 快速搜尋

Settings 之 SearchIndexablesProvider
首先需要在清單檔案中註冊action為"android.content.action.SEARCH_INDEXABLES_PROVIDER"的provider,如下:
      <provider
            android:name=".search.SettingsSearchIndexablesProvider"
            android:authorities="com.android.settings"
            android:multiprocess="false"
            android:grantUriPermissions="true"
            android:permission="android.permission.READ_SEARCH_INDEXABLES"
            android:exported="true">
            <intent-filter>
                <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" /> //註冊此 action
            </intent-filter>
        </provider>

搜尋資料庫路徑:/data/user_de/0/com.android.settings/databases/search_index.db
//search_index.db 資料庫的prefs_index表格中存放的就是搜尋的設定選項
此資料庫的初始化不是在開機階段,而是在每一次開啟settings或者當前切換使用者(因為系統為每一個使用者維護一個單獨的search_index.db),或者是當前的語言發生變化會更新資料庫.

資料庫的初始化:
Index#update

    public void update() {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                // 查詢系統中所有的配置了"android.content.action.SEARCH_INDEXABLES_PROVIDER"的Provider
                final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
                List<ResolveInfo> list =
                        mContext.getPackageManager().queryIntentContentProviders(intent, 0);

                final int size = list.size();
                for (int n = 0; n < size; n++) {
                    final ResolveInfo info = list.get(n);
                    if (!isWellKnownProvider(info)) {
                        continue;
                    }
                    final String authority = info.providerInfo.authority;
                    final String packageName = info.providerInfo.packageName;
                    //列印packageName為:
                    //01-01 17:38:09.257  5207  5511 E qcdds   : packageName= com.android.cellbroadcastreceiver
                    //01-01 17:38:09.678  5207  5511 E qcdds   : packageName= com.android.phone
                    //01-01 17:38:09.777  5207  5511 E qcdds   : packageName= com.android.settings

                    // 新增其他APP的設定項
                    addIndexablesFromRemoteProvider(packageName, authority); //主要方法
                    // 新增其他APP中不需要被搜尋到的設定項
                    addNonIndexablesKeysFromRemoteProvider(packageName, authority);
                }

                mDataToProcess.fullIndex = true;
                // 上面的addIndexablesFromRemoteProvider會新增設定項到記憶體中的一個mDataToProcess物件裡,updateInternal將該物件更新到資料庫中
                updateInternal();
            }
        });
    }


    private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
        try {
            // rank是按照指定演算法計算出的一個值,用來搜尋的時候,展示給使用者的優先順序
            final int baseRank = Ranking.getBaseRankForAuthority(authority);
            // mBaseAuthority是com.android.settings,authority是其他APP的包名
            final Context context = mBaseAuthority.equals(authority) ?
                    mContext : mContext.createPackageContext(packageName, 0);
            // 構建搜尋的URI
            final Uri uriForResources = buildUriForXmlResources(authority);
            // 兩種新增到資料庫的方式,我們以addIndexablesForXmlResourceUri為例
            addIndexablesForXmlResourceUri(context, packageName, uriForResources,
                    SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank);

            final Uri uriForRawData = buildUriForRawData(authority);
            addIndexablesForRawDataUri(context, packageName, uriForRawData,
                    SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
                    + Log.getStackTraceString(e));
            return false;
        }
     }


上面程式碼主要做了下面事情:

    根據當前包名建立對應包的context物件。
    根據當前包名構建指定URI,例如,settings:content://com.android.settings/settings/indexables_xml_res
    然後通過context物件查詢對應的Provider的資料
    之所以構建出content://com.android.settings/settings/indexables_xml_res 這樣的URI是因為所有的需要被搜尋到的設定項所在的APP,其Provider都需要繼承自SearchIndexablesProvider


//SearchIndexablesProvider 繼承 ContentProvider
public abstract class SearchIndexablesProvider extends ContentProvider {
    ....
        //定義了查詢路徑
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_XML_RES_PATH,
                MATCH_RES_CODE);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_RAW_PATH,
                MATCH_RAW_CODE);
        mMatcher.addURI(mAuthority, SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH,
                MATCH_NON_INDEXABLE_KEYS_CODE);
    ....

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {
        switch (mMatcher.match(uri)) {
            // 匹配不同的Uri進行查詢
            case MATCH_RES_CODE:
                return queryXmlResources(null);
            case MATCH_RAW_CODE:
                return queryRawData(null);
            case MATCH_NON_INDEXABLE_KEYS_CODE:
                return queryNonIndexableKeys(null);
            default:
                throw new UnsupportedOperationException("Unknown Uri " + uri);
        }
    }

    @Override
    public String getType(Uri uri) {
        switch (mMatcher.match(uri)) {
            case MATCH_RES_CODE:
                return SearchIndexablesContract.XmlResource.MIME_TYPE;
            case MATCH_RAW_CODE:
                return SearchIndexablesContract.RawData.MIME_TYPE;
            case MATCH_NON_INDEXABLE_KEYS_CODE:
                return SearchIndexablesContract.NonIndexableKey.MIME_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }
    ....

}


//採用 MatrixCursor 構建虛擬的資料表
public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
    private static final String TAG = "SettingsSearchIndexablesProvider";

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor queryXmlResources(String[] projection) {
        MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
        //通過 SearchIndexableResources.values() 獲取到所有新增到map集合中的 SearchIndexableResource
        /*
          SearchIndexableResource的路徑為: /frameworks/base/core/java/android/provider/SearchIndexableResource.java
          其中定義了    this.rank = rank;
                        this.xmlResId = xmlResId;
                        this.className = className;
                        this.iconResId = iconResId;
          等屬性
        */
        Collection<SearchIndexableResource> values = SearchIndexableResources.values(); 
        for (SearchIndexableResource val : values) {
            Object[] ref = new Object[7];
            ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
            ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
            ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
            ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
            ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = null; // intent action
            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package
            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
            cursor.addRow(ref);
        }
        return cursor;
    }

    @Override
    public Cursor queryRawData(String[] projection) {
        MatrixCursor result = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
        return result;
    }
    // 該方法返回當前佈局不想被搜尋到的設定項
    @Override
    public Cursor queryNonIndexableKeys(String[] projection) {
        MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
        return cursor;
    }
}



接著
//Index#addIndexablesForXmlResourceUri

private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
            Uri uri, String[] projection, int baseRank) {
        // 獲取指定包對應的ContentResolver
        final ContentResolver resolver = packageContext.getContentResolver();
        final Cursor cursor = resolver.query(uri, projection, null, null, null);

        if (cursor == null) {
            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
            return;
        }

        try {
            final int count = cursor.getCount();
            if (count > 0) {
                while (cursor.moveToNext()) {
                    final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;

                    final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);

                    final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
                    final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);

                    final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
                    final String targetPackage = cursor.getString(
                            COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
                    final String targetClass = cursor.getString(
                            COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);

                    SearchIndexableResource sir = new SearchIndexableResource(packageContext);
                    sir.rank = rank;
                    sir.xmlResId = xmlResId;
                    sir.className = className;
                    sir.packageName = packageName;
                    sir.iconResId = iconResId;
                    sir.intentAction = action;
                    sir.intentTargetPackage = targetPackage;
                    sir.intentTargetClass = targetClass;
                    // 解析cursor資料,並且新增到記憶體UpdateData的dataToUpdate屬性上, dataToUpdate屬性是一個list集合
                    addIndexableData(sir);
                }
            }
        } finally {
            cursor.close();
        }
}



public void addIndexableData(SearchIndexableData data) {
        synchronized (mDataToProcess) {
            mDataToProcess.dataToUpdate.add(data);
        }
}

//Index#updateInternal更新到資料庫中
private void updateInternal() {
        synchronized (mDataToProcess) {
            final UpdateIndexTask task = new UpdateIndexTask();
            // 拷貝一個mDataToProcess物件的副本,前面將資料新增到mDataToProcess物件中。
            UpdateData copy = mDataToProcess.copy();
            // 執行UpdateIndexTask,UpdateIndexTask會將copy物件儲存到資料庫裡
            task.execute(copy);
            mDataToProcess.clear();
        }
}


//接下來的呼叫流程為:
Index$UpdateIndexTask
  doInBackground --> 
      processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, forceUpdate) --> // 插入或者更新當前資料庫內容   
          indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys) -->  // 繼續indexOneSearchIndexableData更新資料庫



private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
            SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
        //兩種方式新增資料庫
        if (data instanceof SearchIndexableResource) {
            indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
        } else if (data instanceof SearchIndexableRaw) {
            indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
        }
}
        接下來呼叫:
        indexFromResource() -->  //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled) 獲取當前佈局不想被搜尋到的設定項
                indexFromProvider() --> //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled)  需要解析的佈局
                      indexFromResource() -->  //使用XmlResourceParser解析xml佈局
                            updateOneRowWithFilteredData() --> 
                                  updateOneRow() -->  //此方法最終將解析的資料更新至資料庫

//在settings中新增搜尋項:

在SearchIndexableResources 中維護了一個sResMap,其中添加了所有的SearchIndexableResource,每一個子頁面對應一個SearchIndexableResource,並且在SearchIndexableResources 提供了一個values()方
法,用來返回當前集合中的所有資料,其實SearchIndexableResources.values()是在SettingsSearchIndexablesProvider中用到的,SettingsSearchIndexablesProvider重寫了queryXmlResources方法,並且通過SearchIndexableResources.values()會返回setting中所有的子頁面,最後封裝成一個cursor.

在SearchIndexableResources的靜態程式碼塊中初始化了:
    static {
        sResMap.put(WifiSettings.class.getName(),
                new SearchIndexableResource(
                        Ranking.getRankForClassName(WifiSettings.class.getName()),
                        NO_DATA_RES_ID,
                        WifiSettings.class.getName(),
                        R.drawable.ic_settings_wireless));
    
        sResMap.put(SavedAccessPointsWifiSettings.class.getName(),
                new SearchIndexableResource(
                        Ranking.getRankForClassName(SavedAccessPointsWifiSettings.class.getName()),
                        R.xml.wifi_display_saved_access_points,
                        SavedAccessPointsWifiSettings.class.getName(),
                        R.drawable.ic_settings_wireless));

    }
兩種方式,一種是直接在new SearchIndexableResource() 時傳入佈局檔案,另一種為"NO_DATA_RES_ID"表示此搜尋項匹配沒有需要解析的xml檔案,此xml的解析在Index.java中,
採用第二種時,需要在對應的類中建立一個SEARCH_INDEX_DATA_PROVIDER,型別為SearchIndexProvider,繼承BaseSearchIndexProvider並複寫其兩個方法: 
getXmlResourcesToIndex 和 getNonIndexableKeys.

以SecuritySettings為例:
    /**
     * For Search. Please keep it in sync when updating "createPreferenceHierarchy()"
     */
    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new SecuritySearchIndexProvider();

    private static class SecuritySearchIndexProvider extends BaseSearchIndexProvider {

        @Override
        public List<SearchIndexableResource> getXmlResourcesToIndex(
                Context context, boolean enabled) {
            final List<SearchIndexableResource> index = new ArrayList<SearchIndexableResource>();
            //返回需要解析的佈局
            index.add(getSearchResource(context, R.xml.security_settings_misc));
            return index;
        }

        @Override
        public List<String> getNonIndexableKeys(Context context) {
            // 該方法返回當前佈局不想被搜尋到的設定項
            final List<String> keys = new ArrayList<String>();
            LockPatternUtils lockPatternUtils = new LockPatternUtils(context);

            // Do not display SIM lock for devices without an Icc card
            final UserManager um = UserManager.get(context);
            final TelephonyManager tm = TelephonyManager.from(context);
            if (!um.isAdminUser() || !tm.hasIccCard()) {
                keys.add(KEY_SIM_LOCK);
            }
            if (um.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) {
                keys.add(KEY_CREDENTIALS_MANAGER);
            }

            // TrustAgent settings disappear when the user has no primary security.
            if (!lockPatternUtils.isSecure(MY_USER_ID)) {
                // keys.add(KEY_TRUST_AGENT); //Move To LockScreen Settings
                keys.add(KEY_MANAGE_TRUST_AGENTS);
            }

            return keys;
        }



在搜尋過程中,會從資料庫中查詢匹配,點選篩選結果,根據className啟動對應的介面,程式碼實現在SearchResultsSummary類中:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        final View view = inflater.inflate(R.layout.search_panel, container, false);

        mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
        mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);

        mResultsListView = (ListView) view.findViewById(R.id.list_results);
        mResultsListView.setAdapter(mResultsAdapter);
        mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { // mResultsListView就是查詢的結果列表
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                // We have a header, so we need to decrement the position by one
                position--;

                // Some Monkeys could create a case where they were probably clicking on the
                // List Header and thus the position passed was "0" and then by decrement was "-1"
                if (position < 0) {
                    return;
                }

                final Cursor cursor = mResultsAdapter.mCursor;
                cursor.moveToPosition(position);

                final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); 
                final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE);
                final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION);
                final String key = cursor.getString(Index.COLUMN_INDEX_KEY);

                final SettingsActivity sa = (SettingsActivity) getActivity();
                sa.needToRevertToInitialFragment();
                if (TextUtils.isEmpty(action)) {
                    Bundle args = new Bundle();
                    args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);

                    Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); // 通過className啟動Settings中對應的Fragment介面
                } else {
                    final Intent intent = new Intent(action);

                    final String targetPackage = cursor.getString(
                            Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
                    final String targetClass = cursor.getString(
                            Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS);
                    if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
                        final ComponentName component =
                                new ComponentName(targetPackage, targetClass);
                        intent.setComponent(component);
                    }
                    intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);

                    sa.startActivity(intent);
                }

                saveQueryToDatabase();
            }
        });

    }

相關推薦

Android Settings 快速搜尋

Settings 之 SearchIndexablesProvider 首先需要在清單檔案中註冊action為"android.content.action.SEARCH_INDEXABLES_PROVIDER"的provider,如下: <provide

Android 從資料庫中快速搜尋匹配資料並新增監聽事件

如何從資料庫中搜索與我們目標相符的資料呢? 我使用的是List view+cursoradapter。現在應該很少有人使用list view了吧,原來打算換換recyclerview來寫的,但是recyclerview不支援cursor view啊。暫且先記著

start com.android.settings/com.android.settings.SubSettings activity

window content htm track dap method popu -s use 1. get class name: adb shell [email protected]/* */:/mnt/sdcard/books $ dumpsys w

Android快速實現自定義字體!

sdk true fcm version ttf spa pre ets 怎麽 前言:我們都知道,Android中默認的字體是黑體,而大多數app也都是使用的這種字體,但我們發現,大多數app中,個別地方字體非常好看,例如app的標題欄,菜單欄等地方,那他們是怎麽做到的呢?

Android基礎——快速開發之定制BaseTemplate

temp .net fonts The 成了 抽取 一份 應該 我們 初學者肯定會遇到一個日常任務,那麽就是findViewById,setOnClickListener(暫且把它們稱為日常任務),而且很多人會把他們混在一起,導致項目結構混亂,最主要的是寫多了會煩,不覺得嗎

Android基礎——快速開發之打造萬能適配器

臃腫 log 思想 代碼分析 htm 考試報名 做了 順序 基礎 這裏以ListView作演示,對於ListView我們再熟悉不過了,其步驟分為: 創建ListView的Bean對象 創建ListView的Adapter的ItemView布局 創建ListView的Ada

Android BLE 快速上手指南

原文地址 本文旨在提供一個方便沒接觸過Android上低功耗藍芽(Bluetooth Low Energy)的同學快速上手使用的簡易教程,因此對其中的一些細節不做過分深入的探討,此外,為了讓沒有Ble裝置的同學也能模擬與裝置的互動過程,本文還提供了中央裝置(central)和外圍裝置(periphera

Android實現快速傳送電子郵件

最近有朋友有需求是通過apk傳送郵件,我心想這怎麼可以實現?然後就研究了一番,最後得出結論是可行的! 確實可以自己的手機上定義主題和內容或者附件,然後傳送給對應的郵箱!詳細步驟傾聽我一一道來 我們以A郵箱傳送郵件給B郵箱為例: 1 開啟A郵箱的POP3服務 每個郵箱都有POP3服

android——butterKnife快速生成

gradle: compile 'com.jakewharton:butterknife:8.5.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' 步驟: 下載外掛 file-》settings

BAT大牛親授技能+技巧 Android面試快速充電升級

前往下載 BAT大咖助力 全面升級Android面試 第1章 課程介紹(本課程專為初中級同學面試複習) 本課程專為初中級程度同學面試準備的系統複習指南,本章帶你瞭解面試過程中會遇到的問題,個人應該擺正的心態,以及面試官最為看重你的解決問題的思路。關於框架面試專題課程請移步到:http

消滅黑白屏,實現android app“快速啟動”

進行應用開發時,如果沒有對app的啟動頁做處理,那我們的app冷啟動時就會出現一個白屏或者黑屏的過程,正是這個黑白屏過程的存在會讓使用者感覺app啟動速度慢,本篇部落格中所說的“快速啟動“”也正是針對這個過程進行優化以達到沒有黑白屏的過程; 關於app的冷啟動: 冷啟動是指在程序未建立時,使用者

adb命令列開啟Android settings

adb命令開啟手機設定頁面 設定主頁面 adb shell am start com.android.settings/com.android.settings.Settings 安全 adb shell am start com.android.settings/com.andro

Android annotations快速開發框架使用,Android Studio與Eclipse配置

Androidannotations框架是目前最火的Andorid端快速開發框架,通過註解方式挺高開發效率,減少重複編寫沒有技術含量的程式碼。       使用AndoridAnnotations框架的理由:      

快速搜尋效能問題調研

    最近因為專案需要做搜尋,安排我對搜尋的效能這一方面做調研。本文件調研了simhash和es為代表的搜尋方案。用Simhash和ElasticSearch做搜尋各有優缺點,綜合來看可這麼標籤:Simhash是偏計算密集型的搜尋方案代表,但演算法方案複雜;ElasticSea

Android 鍵盤的搜尋按鈕功能

系統鍵盤的搜尋按鈕,預設情況下是被隱藏的,如果要使用必須要手動設定,才可以調用搜索按鍵功能。 具體使用,只需要如下三個步驟: 1:在佈局檔案中的EditText中新增如下三個屬性 android:maxLines=“1” android:singleLine=“true” and

Android藍芽搜尋連線通訊

  藍芽( Bluetooth® ):是一種無線技術標準,可實現固定裝置、移動裝置和樓宇個人域網之間的短距離資料交換(使用2.4—2.485GHz的ISM波段的UHF無線電波)。藍芽技術最初由電信巨頭愛立信公司於1994年創制,當時是作為RS232資料線的替代方案。

BAT大牛親授技能 技巧 Android面試快速充電升級

第1章 課程介紹(本課程專為初中級同學面試複習)本課程專為初中級程度同學面試準備的系統複習指南,本章帶你瞭解面試過程中會遇到的問題,個人應該擺正的心態,以及面試官最為看重你的解決問題的思路。關於框架面試專題課程請移步到:http://coding.imooc.com/class/157.html1-1 課程介

【機器人學:運動規劃】快速搜尋隨機樹(RRT---Rapidly-exploring Random Trees)入門及在Matlab中演示

  快速搜尋隨機樹(RRT -Rapidly-ExploringRandom Trees),是一種常見的用於機器人路徑(運動)規劃的方法,它本質上是一種隨機生成的資料結構—樹,這種思想自從LaValle在[1]中提出以後已經得到了極大的發展,到現在依然有改進的RRT不斷地被提出來。

Android避免快速雙擊按鈕最簡單好用的方式

oid 方法 nbsp lis lean 按鈕 urn turn true 代碼如下,直接放到工具類中即可。類可以實現Onclicklistener,然後重寫onClick方法,直接將該函數寫在onClick方法中即可,這樣對於所有的點擊事件都將生效。 避免了快速雙擊出現

Android APP 快速開發教程(安卓)

Android APP 快速開發教程(安卓) 前言 本篇部落格從開發的角度來介紹如何開發一個Android App,需要說明一點是,這裡只是提供一個如何開發一個app的思路,並不會介紹很多技術上的細節,從整個大局去把握如何去構思一個app的開發,讓你對獨立開發一款app的時候有個理解