[SnapEvent]Android Google Map SDK的應用與注意事項

本篇將接續前一篇得到的活動資料。目標為,實作會自動偵測使用者輸入,並顯示地點提示的搜尋框。在使用者按下搜尋框的提示後,畫面轉換到Map Activity並標記了剛剛選擇的提示地點,然後將之前得到的所有活動地點也顯示在Map上給使用者參考。

本篇使用Google Maps Android API第2版,基本的使用教學可以參考Tony的地圖與定位 1-3 (Maps and Positioning 1-3),我只記錄我在實作過程中,容易出錯的地方,還有大方向的開發流程。

1. API KEY

(1) 申請API key並啟用google map服務

申請的網址可以參考這裡,申請完後別忘記為你的應用程式啟用需要的API,如果沒有如下圖一樣啟用Google map APIs,在Android呼叫時service會沒有回應。

tag
tag

如果一開始就確定會用到Google map SDK,建議剛開始建立專案時就選擇Google Map Activity,就不用再打理麻煩的權限設定等…只要引入API key就好了,如下圖。

Google Map Activity

(2) 別忘記下載Google Play Service SDK

Google Map API為裡面的一部分。其實就算你沒有下載,gradle也會自動提醒你,這就是自動化工具的好處之一。

tag

(3) 放置API Key的地方

將google map API key放到AndroidManifest.xml,你可能會注意到有2種Key的宣告,分別是com.google.android.maps.v2.API_KEY和com.google.android.geo.API_KEY。那要用哪一種?其實二種key的值都一樣,而com.google.android.maps.v2.API_KEY只有提供Map API service,com.google.android.geo.API_KEY則是Map API service和Place API service都有提供,考量到2個API key不能同時宣告,強烈推薦直接使用com.google.android.geo.API_KEY。

tag

2. 第一步:AutoCompleteTextView與Google Place的結合

思路

使用AutoCompleteTextView元件,讓使用者在輸入地方資訊時,自動根據使用者輸入的內容提供地點提示讓使用者選擇。並在使用者選擇地點後,將之引導到標註了剛剛搜尋的地點的Map Activity,如下圖。

tag tag

(1) Google官方範例參考:android-play-places的PlaceComplete專案

(2) AutoCompleteTextView元件使用介紹

AutoCompleteTextView實際上就是TextView的一種,只是他多增加一種功能,會根據使用者輸入的內容提供選項讓使用者選擇。增加提示的方法和填充ListView內文的方法相同,為它設置一個Adapter,裡面封裝了要提供給AutoCompleteTextView的提示文字即可,下面做個最簡單的java code示範,AutoCompleteTextView是Android SDK內建的View元件,可以在xml佈局時直接從UI Design mode內拖拉。

public class AutoCompleteTextViewActivity extends Activity {  

    String[] samples = new String[] { "One", "Two", "Three", "Four"};  
  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.autocompletetextview);  

        ArrayAdapter<String> aa = new ArrayAdapter<String>(this,  
                android.R.layout.simple_dropdown_item_1line, samples);  //建立Adapter
        AutoCompleteTextView actv = (AutoCompleteTextView) findViewById(R.id.auto); //得到View物件
        actv.setAdapter(aa);  //設置Adapter
    }  
}  

其中的ArrayAdapter,有實作Filterable interface。Filterable interface的功能是用來,從現有的全部結果中,依照你輸入的字元,過濾出符合的結果。而AutoCompleteTextView,則只單純地列出Filterable所傳回的結果。例如,當你輸入個B字元時,AutoCompleteTextView就藉由Filterable的函式,列出只有B開頭的國家。

(3) Google Example code提供的PlaceAutocompleteAdapter.java

PlaceAutocompleteAdapter繼承了ArrayAdapter,並實作了Filterable的Filter method,回傳的Filter功能是把使用者輸入的關鍵字,丟到Google Map service搜尋符合的結果,再將結果傳回來顯示在AutoCompleteTextView的列表提示,下面只截取重要PlaceAutocompleteAdapter.java的Filterable實作部分。

@Override
    public Filter getFilter() {
        Filter filter = new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) { //實作過濾功能
                FilterResults results = new FilterResults();
                // 如果AutoCompleteTextView內無輸入任合內容則略過過濾
                if (constraint != null) {
                    // 由讀者輸入的關鍵字constraint尋找提示
                    mResultList = getAutocomplete(constraint);
                    if (mResultList != null) { //如果結果回傳成功
                        results.values = mResultList;
                        results.count = mResultList.size();
                    }
                }
                return results;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                if (results != null && results.count > 0) {
                    // 回傳的提示至少一個以上才會通知更新
                    notifyDataSetChanged();
                } else {
                    notifyDataSetInvalidated();
                }
            }
        };
        return filter;
    }

讓我們來看看getAutocomplete(CharSequence constaint)裡,如何使用Google Map Places service補充AutoCompleteTextView的提示。

   private ArrayList<PlaceAutocomplete> getAutocomplete(CharSequence constraint) {
        if (mGoogleApiClient.isConnected()) {
            Log.i(TAG, "Starting autocomplete query for: " + constraint);

            // 輸入使用者輸入的關鍵字,回傳的PendingResult為AutocompletePredictionBuffer物件的List,內為Google Map Places service搜尋的結果
            //mBounds為地點的搜索範圍(本App限台灣)
            PendingResult<AutocompletePredictionBuffer> results =
                    Places.GeoDataApi
                            .getAutocompletePredictions(mGoogleApiClient, constraint.toString(),
                                    mBounds, mPlaceFilter);

            // 等待回傳所有的地點結果,並最多等待60秒
            AutocompletePredictionBuffer autocompletePredictions = results
                    .await(60, TimeUnit.SECONDS);

            // 確認結果回傳成功,否則回傳null
            final Status status = autocompletePredictions.getStatus();
            if (!status.isSuccess()) {
                Toast.makeText(getContext(), "Error contacting API: " + status.toString(),
                        Toast.LENGTH_SHORT).show();
                Log.e(TAG,"Error getting autocomplete prediction API call. ");
                autocompletePredictions.release();
                return null;
            }

            Log.i(TAG, "Query completed Received .");

            //將結果的地點們(autocompletePredictions)轉移至ArrayList<PlaceAutocomplete>。
            Iterator<AutocompletePrediction> iterator = autocompletePredictions.iterator(); //得迭代器
            ArrayList resultList = new ArrayList<>(autocompletePredictions.getCount());
            while (iterator.hasNext()) {
                AutocompletePrediction prediction = iterator.next();

                // PlaceAutocomplete為beans,內只封裝地點的ID和Description
                resultList.add(new PlaceAutocomplete(prediction.getPlaceId(),
                        prediction.getDescription()));
            }

            // 要釋放AutocompletePredictionBuffer物件,以預防記憶體流失
            autocompletePredictions.release();

            return resultList;
        }
        Log.e(TAG, "Google API client is not connected for autocomplete query.");
        return null;
    }

上面提到的Place bean,PlaceAutocomplete我就不贅述了,各位可以在這裡看到源碼

(4) 使用PlaceAutocompleteAdapter的AutoCompleteTextView

以下為引用PlaceAutocompleteAdapter的程式部分。

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mAutocompleteView = (AutoCompleteTextView)
                getView().findViewById(R.id.autocomplete_places);

        mAutocompleteView.setOnItemClickListener(mAutocompleteClickListener); //按下提示後呼叫mAutocompleteClickListener
        mAdapter = new PlaceAutocompleteAdapter(getActivity(), android.R.layout.simple_list_item_1,
                mGoogleApiClient, BOUNDS_GREATER_SYDNEY, null); //實體化PlaceAutocompleteAdapter
        mAutocompleteView.setAdapter(mAdapter); //設定Adapter
    }

以下說明PlaceAutocompleteAdapter的各個參數。

* mGoogleApiClient: 初始化google map client(使用Google Map API皆需要)

GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
                .enableAutoManage(getActivity(), 0 /* clientId */, this)
                .addApi(Places.GEO_DATA_API)
                .build(); //set google map client init

* BOUNDS_GREATER_SYDNEY: 限制地點提示的範圍為台灣本島

private static final LatLngBounds BOUNDS_GREATER_SYDNEY = new LatLngBounds(
            new LatLng(21.715956, 119.419628),new LatLng(25.371160, 122.138744)); //Taiwan scope

範圍的選擇如下,由左至右;下至上。

Taiwan scope

(5) 使用AutoCompleteTextView的setOnItemClickListener method達到開啟Map Activity的目的

private AdapterView.OnItemClickListener mAutocompleteClickListener
            = new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<> parent, View view, int position, long id) {
  
            final PlaceAutocompleteAdapter.PlaceAutocomplete item = mAdapter.getItem(position);//拿到place bean
            final String placeId = String.valueOf(item.placeId);
            Log.i("Input place", "Autocomplete item selected: " + item.description);

            /*
             使用placeId當參數,像Places Geo Data API要求PlaceBuffer物件(位置詳細資訊)
              */
            PendingResult<PlaceBuffer> placeResult = Places.GeoDataApi
                    .getPlaceById(mGoogleApiClient, placeId);

            //當placeResult回傳結果後,呼叫mUpdatePlaceDetailsCallback method
            placeResult.setResultCallback(mUpdatePlaceDetailsCallback); 
            Log.i("Input place", "Called getPlaceById to get Place details for " + item.placeId);
        }
    };

在mUpdatePlaceDetailsCallback method內,我們必須開啟新的Activity(Map Activity),並將位置資訊傳到新Activity,讓其地點標記在Map上。

private ResultCallback<PlaceBuffer> mUpdatePlaceDetailsCallback
            = new ResultCallback<PlaceBuffer>() {
        @Override
        public void onResult(PlaceBuffer places) {
            if (!places.getStatus().isSuccess()) { //如果回傳失敗
                Log.e("TAG", "Place query did not complete. Error: " + places.getStatus().toString());
                places.release(); //釋放PlaceBuffer,以預防記憶體流失
                return;
            }
            //第一個結果
            final Place place = places.get(0);

            Intent info = new Intent(getActivity() , MapsActivity.class); //Intent指向MapsActivity
            info.putExtra("latitude", place.getLatLng().latitude); //儲存緯度資料
            info.putExtra("longitude",place.getLatLng().longitude); //儲存經度資料
            info.putExtra("name",place.getName()); //儲存地點名稱
            Log.i("place",place.getLatLng().toString());
            getActivity().startActivity(info); //開啟新Activity

            places.release(); //釋放PlaceBuffer,以預防記憶體流失
        }
    };

這樣一整套思路就完成了!接下來將進入顯示活動標記。

##3. 第二步:標記剛剛的提示,並將多個活動位置顯示在Google Map上

(1) 標記前一個Activity所選擇的提示地點,並顯示在Map上

   Intent info = getIntent();
   LatLng place = new LatLng(info.getDoubleExtra("latitude", 0), info.getDoubleExtra("longitude", 0));
   String name = info.getStringExtra("name"));
   mMap.addMarker(new MarkerOptions().position(place).title(name)).showInfoWindow(); //在Mapp加入marker
   mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place, 15.0f)); //移動Map鏡頭,關注在marker所在地,縮放程度為15f
   CameraUpdate zoom = CameraUpdateFactory.zoomTo(14f); //縮放程度,此為主
   mMap.animateCamera(zoom, 1000, new GoogleMap.CancelableCallback() { //1000 is animate velocity
            @Override
            public void onFinish() { //鏡頭更新成功後
                Log.i("animateCamera", "onFinish");
                markers = getEventAddress();
                for (int i = 0; i < markers.size(); i++) {
                    new GeocoderTask().execute(markers.get(i)); //執行非同步,並傳入參數MarkerItem物件
                }
            }
            @Override
            public void onCancel() {
            }
        });

上面的getEventAddress()會將所有原來只有地址的活動,封裝成MarkerItem物件,MarkerItem內封裝活動的順序,地址和Address物件,MarkerItem bean就不在下面贅述,code可以在這裡觀看。

public List<MarkerItem> getEventAddress() {

        HashMap events = AppController.getInstance().getEventbeans();
        List<xmlEventBean> ev = (List<xmlEventBean>) events.get("KKTIX"); //拿到所有活動
        Log.i("getTimeANDplace", "1: " + ev.size());
        List<MarkerItem> address = new ArrayList<MarkerItem>();
        int i = 0;
        for (xmlEventBean e : ev) {

            //以下做切字,切出活動地點的地址,並加入List<MarkerItem>
            if (!e.getTitle().contains("測試")) {
                String[] arrays = e.getTimeANDplace().split("/ +");
                if (arrays.length >= 2 && !arrays[1].equals("無")) {
                    if (!arrays[1].contains(" ")) {
                        Log.i("getTimeANDplace:arrays", arrays[1]);
                        address.add(new MarkerItem(i, arrays[1], null));
                    } else {
                        String[] ans = arrays[1].split(" ");
                        if (ans.length >= 1) {
                            Log.i("getTimeANDplace:ans", ans[0]);
                            address.add(new MarkerItem(i, ans[0], null));
                        }
                    }
                }
            }
            i++;
        }
        Log.i("getTimeANDplace", "2: " + address.size());//因為上面做了篩選,數量會比1:時來的少
        return address;
    }

(2) 使用GeocoderTask extends AsyncTask執行非同步(多線程),實現“標注活動地點於地圖上”的工作

private class GeocoderTask extends AsyncTask<MarkerItem, Void, MarkerItem> {

        @Override
        protected MarkerItem doInBackground(MarkerItem... locationName) { //在另一個Thread處理(非同步處)
        
            //實體化Geocoder物件,他有“從經緯度尋找地址”和“從地址尋找經緯度”這2個功能
            Geocoder geocoder = new Geocoder(getBaseContext());
            List<Address> addresses = null;
            MarkerItem name = locationName[0]; //拿到MarkerItem物件
            try {

                // 使用MarkerItem物件所封裝的活動地址取得Address物件
                // 並只取回傳結果的第一個Address物件放進MarkerItem物件
                addresses = geocoder.getFromLocationName(name.getAddress(), 1);
                if (addresses != null && addresses.size() >= 1) {
                    name.setPlaceItem(addresses.get(0)); 
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return name; 回傳MarkerItem物件至onPostExecute
        }

        @Override
        protected void onPostExecute(MarkerItem addresses) { //回到UI Thread執行,不能放耗時操作

            if (addresses == null || addresses.getPlaceItem() == null) {
                Log.i("FindNoLocation", "on " + addresses.getAddress());
            }
            // 從傳入的MarkerItem取得Address物件
            Address address = (Address) addresses.getPlaceItem();

            // 如果Address不為空,實體化MarkerOptions,標記活動地點於map上
            if (address != null) {
                LatLng latLng = new LatLng(address.getLatitude(), address.getLongitude());
                markerOptions = new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.direction_down)); //指定客製化marker icon
                markerOptions.position(latLng);
                mMap.addMarker(markerOptions.title(addresses.getAddress())); //作為marker query之用
            }
        }
    }

從上面程式來看,為何要設定每個Marker的title?其實是為了之後OnMarkerClickListener所用,請看下面程式。

mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {

            @Override
            public boolean onMarkerClick(Marker arg0) {

                if(markers !=null){
                    for (MarkerItem it : markers) {

                        //為了得知現在所選的Marker是哪個活動地點,一個個測試其活動地址是否符合
                        if (arg0.getTitle().equals(it.getAddress())) { 

                            if(preClicked!=null){
                                preClicked.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.direction_down));
                            }
                            Log.i("clickedIcon",it.getAddress());
                            arg0.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.direction_down2));
                            if (markinfo.getVisibility()== View.GONE) {
                                markinfo.setVisibility(View.VISIBLE); //如果細節活動框不可見,則顯示細節框
                            }
                            //設定活動細節框內容
                            infotitle.setText(it.getBean().getTitle());
                            infodes.setText(it.getBean().getTimeANDplace());
                            preClicked=arg0;
                        }
                    }
                    return true;
                }else{
                    return false;
                }
            }
        });

以下,為以上活動標記實作成品。

tag tag

##4. project repo git clone下面專案到自己的電腦後,使用 git checkout e687 .。

SnapEvent
[jekyll+Github架站]Domain網址客製化

##源由

從用Github page site的mis101bird.github.io網址建了個人網站後,經營了快一個月了,也沒有想過要換網址。只是我媽一直嫌,說在搜尋引擎找不到我的個人網站的進入點,就算我之前已經在各大搜尋入口網站註冊過了。我想說不定是因為網址的關係,如果把個人網站改成客製化的domain name(網域名稱),搜尋結果可能會改善…基於這樣的原因,我開始研究如何換址,發現Github還提供了domain name客製化的教學!太棒了,這樣無容量限制又免費的網站空間真的沒問題嗎XD

以下對如何設置客製化domain name做個紀錄。官方教學文檔在這裡,可供大家對照參考。其實改了domain name後,我還沒感覺到什麼流量和搜尋上的變化…唯一的好處只有網址讓人看不出這網站其實是架在Github的。

1) 新增CNAME文件檔

直接在網站repo的根目錄下新增一個CNAME文件檔,裡面寫一個自己客製化的domain name。如果你的repo名稱是用帳號.github.io,也就是所謂的Github page site,則直接在master branch新建CNAME,反之普通的jekyll repo則在gh-pages branch新建CNAME。

github CNAME

是否有設置CNAME成功,可以直接到右邊的setting查看網址是否有改成你CNAME所指定的域名。

2) 在DNS註冊網域名

DNS的工作是將domain name對應到server IP,舉例像我們在瀏覽器輸入的yahoo.com,會被DNS轉成IP: 98.139.183.24。所以我們現在的目標,是在DNS註冊XXX.com,並讓網站repo內的CNAME把XXX.com對應到XXXX.github.io,指到Github的專案後,接下來的就能讓Github自己去處理了。

我們可以在mac的terminal使用dig http://XXX.com指令查看DNS轉址的過程(Window用nslookup指令,只是功能遠沒有dig強大),下面是我設定好後的dig結果。

dig url

由上圖,我們看到 mis101bird.js.org IN CNAME mis101bird.github.io,就是mis101bird.js.org被DNS的CNAME record轉成mis101bird.github.io。DNS不僅能用來解析網域名稱與IP,還可以回應很多關於網路和主機的其它訊息。為了要達到這個功能,必需將不同的記錄設定於DNS裡,而這些記錄就是我們所說的RR TYPE(resource record types),目前較常用的有A、MX、CNAME、NS等record。

下面表格為Record 解說,更多資料請參考這裡

紀錄 範例 說明 使用時機
A紀錄 www.abc.com IN A 232.61.23.15 網域名對應IP 假如你架設一個網站,IP位置為203.71.252.13,只要再申請網域名稱,透過DNS代管設定A記錄,網友就可以經由你所申請的網域名稱到達你的網站,如:www.abc.com。
CNAME紀錄 web.abc.com IN CNAME www.abc.com CNAME記錄就好像是A記錄的分身,幫已存在的A紀錄設定別名 設定完A紀錄後,如果你再利用CNAME記錄,就可以為你的網站再取個別名,如:web.abc.com
MX 紀錄 abc.com IN MX 優先序號 Mail-Server 當有人要寄信到xxx@abc.com(域名)的信箱時,會把信先寄到優先序號高的Mail-Server,若Mail-Server無回應才再依序往優先序號低的Mail-Server發信。 假如你架設一台Mail Server,只要再申請一個網域名稱,透過DNS代管設定MX記錄,你就可以使用擁有自己網域名稱的Email Address來收發信件。ex: service@abc.com

Github官方教學提到,如果想要類似XXX.com的apex domain,要在DNS的A,ALIAS,或ANAME record做設定。如果是想要類似www.XXX.com這種subdomain,在DNS設定CNAME即可,官方教學可參考這裡

網路上提供domain name的系統有付費和免費的,如果付費就能客製化自己的網址後綴,免費的話,他會提供固定幾種後綴讓你挑選(ex: XXX.js.org),也因為不用對使用者負責,比較有DNS掛掉的可能性。我在下面介紹一些DNS服務給大家參考。

1. 免費的DNS: dns.js.org

只要申請pull request,把自己的網站repo註冊到cnames_active.js,再請作者合併回專案即可。速度超快也不用自己設定什麼record,只是後綴被限定為js.org了。

2. 付費的DNS: Simple DNS

這個許多人都推,還有提供自動連結github的服務,也能隨意客製化domain name,可說是應有盡有了。

[2015HackNTU]感想以及我所學到的事

2015HackNTU

是一場以台北城市為題、為期三天兩夜的Hackathon。出乎我預料的是,在這個Hackathon不只是一直coding,早午晚每一個小時還有不同的演講,連Make school的創辦人都來了!真令人興奮。 經由演講我得知了好玩又酷的GML,也學到Hackathon參賽者在這場比賽必須考慮的目的性。最後擺攤位發表自己的作品時,評審們的建議也教導我們想得更深入,像是遠程規劃、如何營利等… 再來,因為此次Hackathon是國際場的,所以外國講者和參賽者都很多,英文聽力在此時就顯得很重要,否則某些演講及Demo就只能睡覺了哈哈。

HackNTU會場

我們的作品構想

我們做的App是結合大地遊戲與台北地方特色的產物,玩家為了要獲得徽章(ex:士林達人),提高排行榜排名,必須完成多個任務,審核通過才能獲得。 審核方式讓玩家互評,每個任務都有正分(讚)和副分(Bad),App後台人員會檢驗這些副分到一定數量的任務,對不合規定的強制下架,並取消勳章的給予。 一個徽章擁有的任務不固定,平均5~7個,任務都是當地最著名的特色地點或小吃,必須親自走出去到當地,完成任務指定的內容(ex在士林夜市玩夾娃娃2隻)並上傳照片,才算完成任務。 App上也有類似FB塗鴉牆的頁面能讓玩家追蹤朋友、被粉絲追蹤等…

###以下是聽從評審建議後的多加想法

審核機制:附分容易使人反感。所以取消副分機制,讓徽章有1周的審核階段,在此期間開放給各玩家觀看並評論,參與評分者能獲得小道具,像變換App介面設計等小功能。

關於營運:營運分三個階段,1. 官方階段:徽章中的任務皆由市政府API提供,從中提取當地排名最高的幾個地點作為任務內容,雖然任務地點較大眾, 但較能激起年輕人對自己家鄉徽章的渴望,舉例就像”身為北投人必須知北投事”的概念,由此增加玩家。2. 客製化階段:等玩家到達一定數量將開放玩家自設徽章和任務內容,提供攻略路線,讓徽章任務本身更在地化,增加遊戲樂趣。

  1. 企業化:企業能藉由建立自己的徽章任務,讓達成任務的玩家獲得實質上的產品,達到宣傳的目的。舉例,賣美麗果的企業,規劃一條美麗果生產路徑之任務,完成一連串任務並得到獎章的人,將獲得真實美麗果產品。不只讓企業做到宣傳、玩家獲得實質產品、App向企業抽成,達到雙贏的目的。

關於作品實作

因為點子太晚才想到,所以實際實作只有1個晚上+1天,但超多超棒的演講又不想錯過,只好只是刻個模+使用的open source。 在這過程,Debug和一些設定文件上的規劃就顯得更重要,才不會多個專案合併時,生出一堆莫名其妙的問題。

AndroidManifest:比賽時因為AndroidManifest隨便亂設,導致合併專案時錯誤不斷,以後希望能按以下寫法。

<?xml version="1.0" encoding="utf-8"?>

#package路徑為從manifest開始直到java檔放置的資料夾位置
#versionCode、versionName為版本號
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.camerafileupload"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="23" />

#開放權限設定
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

#android:name 如果有extends Application的class要在此聲明(全名),此class內大都宣告些全局變數
#android:allowBackup 是否有備份,聽說true有安全性上的疑慮,建議用false
#android:icon HOME上的圖片icon
#android:label HOME上標示的App名稱
#android:theme全局設計主題,大都設定Bar與Tab主題
    <application
        android:name="info.androidhive.camerafileupload.app.AppController"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/MyBarTheme" >

#android:name .LoginActivity=info.androidhive.camerafileupload(package name).LoginActivity
#android:label 各自Activity上Bar的內建靠左Title文字
#android:launchMode 重要,singleTop為如果Activity為Top則不再創建新實體
#android:windowSoftInputMode 選擇性設置,有EditText需要使用者輸入才用設置,會規範鍵盤彈出時的畫面配置
        <activity
            android:name=".LoginActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop"
            android:windowSoftInputMode="adjustPan" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" /> #應用程式進入點
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            </activity>>
        <activity
            android:name=".RegisterActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop"
            android:windowSoftInputMode="adjustPan" />
        <activity
            android:name=".TopCreditActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop" />
        <activity
            android:name=".FirstMainActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop" />
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop" />
        <activity
            android:name=".UploadActivity"
            android:label="@string/app_name"
            android:launchMode="singleTop" />
    </application>

</manifest>

想知道更詳細的android:launchMode的4個選項各自意義請到這裡

想知道更詳細的android:windowSoftInputMode的選項各自意義請到這裡

Fred大大介紹的QML

一開始Demo超酷,對超喜歡介面設計的我超有吸引力的。QML是一個由Qt所研發的使用者圖形介面描述語言,聽說像Tesla等大公司的UI介面是用這個設計的。 Qt則是Nokia底下的一間子公司的跨平台軟體框架,可以編出在不同作業系統、手機等地方跑的執行檔。聽說QML+Node.js很好用,支援套件也多,以後再研究。

  1. brig
  2. QML-Example
  3. QT官網

在網上隨意google一下,發現資料似乎不多,桌上型應用程式比較推薦。

Hackathon參賽者建議

因為只有短短3天,所以作品完成度一定要有取捨(除非是神人),以下約分2種類型:

  1. 試水溫:測試自己的點子是否有前景,只需把作品使用流程的完整性作足夠,裡面的Data可以先做死的假資料,Server等…真的有時間才建。 把重點放在傳達自己的idea,花多些時間在PPT和口說表達,如果觀眾反應良好,就有實作的價值,再開始細部實作。
  2. 宣傳產品:基本上作品已經完成80%,只是藉由此次Hackathon增加曝光率,而且有很多大企業都會到Hackathon偵查,所以也是增加接觸大公司、找贊助的好機會。 如果是這樣目的的人就要把自己作品做到完美!這樣的團隊也大都較容易拿到獎項。

心得與遺憾

其實Hackathon一點都不可怕,真的非常好玩!只是沒有過這麼操的我,一下連熬2天真的是要我命。 但人真是習慣的動物,第一天心理還哀叫怎麼特地來這裡找虐,第二天馬上就習慣了還精神超好!非常推薦沒去過的朋友參加! 我心裡的遺憾是,我雖然出國當交換學生過,但已經有些時間沒講還是生疏了,和老外評審解釋產品,講得滴滴嘟嘟…讓我超難過… 我一定要好好練習英文口說!別讓他再生疏Q__Q

HackNTU作品repo

HackInCity
[SnapEvent]穩定又快速的請求網路xml資料

大綱

本篇文章會先介紹為何Android中,用到網路請求時建議使用Volley框架,和Volley中如何使用GET請求網路資料。接下來會介紹在JAVA中,解析複雜的XML資料時,超級好用的SimpleXML以及如何使用。 最後再解說,我如何利用Volley框架結合SimpleXML解析網站上的RSS xml Data,本文會花多一點篇幅在這部分。

為何使用Volley框架請求網路數據

一談到Android的網路請求方式,相信大家第一個想到的就是HttpURLConnection和HttpClient,我以前也是這樣寫過來的。但若請求的東西單純還好,如果要POST、要傳複雜的參數或請求網路圖片等… 就會感到前所未有的煩躁,重複創建AsyncTask,一直重複copy/past的動作,還要擔心自己的寫法穩定性是否足夠。

幸運的是我亂逛網站時,偶然得知了Volley框架。Volley是Google在2013 Google I/O大會上發表的網路通訊框架,它將Http的通訊細節都經過封裝,讓使用者能輕鬆的請求網路數據。 GitHub中,比較複雜的APP也常看到Volley的蹤跡。但Volley有優點也有缺點,是否使用它要視情況而定。下面是Volley優缺點列表:

優點

  1. 高效能的GET/POST數據請求交互
  2. 幫忙處理複雜的網路圖片加載與緩存
  3. 性能穩定
  4. Google官方出的,保證功能全面

缺點

  1. 不適合大數據的上傳/下載

Volley設計的目標就是請求數據量不大但頻繁的網路資料,所以如果有大數據量的網路下載(ex 下載檔案文件),Volley表現就會不太好。

Volley的GET請求

因為這次我只會使用到Volley的GET,而Volley裡文字數據的請求有三個,StringRequest、JsonObjectRequest和JsonArrayRequest。JsonObjectRequest和JsonArrayRequest可以讓請求JSON數據時更快速,只可惜這次要請求的是XML數據,所以以下說明將使用StringRequest的GET方法做請求範例,JsonObjectRequest和JsonArrayRequest的用法其實和StringRequest相似,詳細會在以後用到時再做介紹,還有Volley最方便的網路圖片請求也會在以後做說明!

1 ) 引用volley包

感恩好心網友幫gradle和Maven建立Volley庫,讓我們能輕鬆使用框架。原repo請至這裡

dependencies {
   compile 'com.mcxiaoke.volley:library:1.0.18'
}

2 ) 宣告請求對列RequestQueue

RequestQueue用來緩存所有HTTP請求。RequestQueue內會按照一定算法發出網路請求。而RequestQueue本身就適合高開發,所以重複宣告多個很浪費資源,故RequestQueue物件大都設為全局Application的變數。

public class MyApp extends Application{
/*......*/
RequestQueue mQueue = Volley.newRequestQueue(context);
/*......*/
}

MyApp別忘記在AndroidManifest.xml加上android:name=”MyApp”,也別忘記加上網路權限

3 ) 創建StringRequest物件

/*
使用POST時用Request.Method.POST;使用GET時用Request.Method.GET
String url 為請求對像server的網址,因為是GET所以參數能直接夾帶在網址後
Response.Listener response successful時調用
Response.ErrorListener response error時調用
*/
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,  
                        new Response.Listener<String>() {  
                            @Override  
                            public void onResponse(String response) {  
                                //response成功時做的事
                            }  
                        }, new Response.ErrorListener() {  
                            @Override  
                            public void onErrorResponse(VolleyError error) {  
                                Log.e("TAG", error.getMessage(), error);  //response失敗時做的事
                            }  
                        });  

4 ) 把建立好的request丟到RequestQueue執行,大功告成!

mQueue.add(stringRequest); 

如果想知道更詳細的Volley使用方法,能參考這裡

Java中,解析XML的方便套件:SimpleXML

台灣有很多活動網站,但不是沒提供完整的API,要不然就是API資料還死守XML不放……尋了好久,最後還是決定,用請求KKTIX的RSS xml資料,來獲得每日活動詳細。 RSS的xml 資料層次多,一層包一層很複雜,如果想快速取出想要的資訊,並跳過不需要的資料,好的XML解析套件就非常重要。 在這個JSON輕量級資料傳輸格式幾乎取代XML的時代,找出好的XML解析套件花了我不少時間,最後終於找到超直觀好用又穩定的套件:SimpleXML了!!

SimpleXML的使用思路是,先跟據要解析的XML文件創建一個java class(用SimpleXML規定的方式撰寫),再通過這個java class去取得XML文件內的具體值。 以下會做些簡易說明:

1 ) 以下為要解析的文件

<?xml version="1.0" encoding="UTF-8"?>  
 <list>  
    <entry id="1">  
        <name>John</name>  
        <gender>Boy</gender>  
    </entry>
 </list>  

2 ) 到官網下載SimpleXML Jar並加到Android Studio或Eclipse的library,官網的example也有教學能讓初學者更快上手。

3 ) 撰寫和要解析的XML文件相對應的java class

@Root(name="list", strict=true) //Root為根,如果<list>內不是每個元素都想去,strict要用false
public class Mylist {  
  @Attribute(required = true)  
    protected String id;  // entry: id="1"

  @Element(name = "name", required = true)  
    protected String Name;  

    @Element(name = "gender", required = true)  
    protected String Gender;  
    
  
    public String getID( ) {  
        return id;  
    }  
      
    public String getName( ) {  
        return Name;  
    }  
  
    public String getGender( ) {  
        return Gender;  
    }  
    
}

怎麼樣!上面的java class對應看起來超直觀的吧!(灑花)

4 ) 通過這個java class去取得XML文件內的具體值

final Serializer serializer;  
serializer = new Persister();  
Mylist list = serializer.read(Mylist.class, String fileData);  //得到Mylist

其中還有能將重複Item都整理成list的@ElementList,歡迎參考這裡

利用Volley框架結合SimpleXML解析網站上的RSS xml Data

本次實作架構的UML如下:

UML
  1. app資料夾:放置App全局資源
    1. AppController.java :繼承了Application,放置全局變量和method的地方
    2. AppRemoteConfig.java:放置資料請求目標端的URL
  2. bean資料夾
    1. Author.java、xmlBeanList.java、xmlEventBean.java:根據KKTIX RSS的xml data撰寫的映射物件。
  3. volleyResponse資料夾:放置Volley相關改寫
    1. SimpleXmlRequest.java:繼承自Volley的Request,給Request做了改寫。

思路始末

我原來的思路,是希望使用Volley的StringRequest把所有xml請求下來,到Response.Listener再用SimpleXML進行XML的解析。但是當我使用StringRequest請求完資料,並在Response.Listener用Log打印出來時,竟然只打印出一半的XML資料,後半資料都消失得莫名其妙,但StringRequest內確實是接收到全部的XML資料,這樣的結果讓我想到Response.Listener內可能不適合放置耗時邏輯,像是我本來將要做的,大量映射XML物件的邏輯… 經過翻找,很幸運地看到itsalif大大的SimpleXmlRequest.java貢獻,這位大大繼承了Volley的Request,並在parseNetworkResponse method中,一接收完資料就用SimpleXML解析XML資料,將映射出來的所有物件再丟給Response.Listener處理,這樣就完美的一個資料都不會漏了! 我就趕快來試一試,真的非常好用!如果各位有想從網路上請求大量的XML資料並解析,非常推薦這個方法,SimpleXmlRequest頁面下也有許多網友做了進階的改寫。再觀看的途中,可能會對 void method( )這種寫法感到疑惑,可以參考這裡

AppController.java內的GetKKTIXRequestToEventBean(Context context)實作

public void GetKKTIXRequestToEventBean(Context context){

eventRequestQueue.cancelAll(VolleyTAG.KKTIX_ALL.getTAG()); //取消還在運行的同一個request

        if(ifInternetOpen(context)) { //如果網路有開啟
           /*
            *AppRemoteConfig使用single pattern,確保不浪費空間資源
            *xmlBeanList.class為給SimpleXML解析資料時的映射物件
            *Response.Listener<xmlBeanList>為Volley資料回傳成功時調用,要實作onResponse method,xmlBeanList為回傳資料型態。
            *Response.ErrorListener為錯誤時調用,要實作 onErrorResponse。
            */
            SimpleXmlRequest<xmlBeanList> simpleRequest = new SimpleXmlRequest<xmlBeanList>          (Request.Method.GET,  
                    AppRemoteConfig.getInstance().getKKTIX_ALL_url(), 
                    xmlBeanList.class,
                    new Response.Listener<xmlBeanList>() {
                        @Override
                        public void onResponse(xmlBeanList response) {
                            List<xmlEventBean> datas = response.getMatches();
                            for (xmlEventBean bean : datas) {
                                Log.i("success", "title: " + bean.getTitle() + " and " + bean.getAuthor().getName());
                            }

                        }
                    },
                    new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError error) {
                            Log.i("error", error.getMessage());
                        }
                    }
            );
            AppController.getInstance().addToRequestQueue(simpleRequest, VolleyTAG.KKTIX_ALL.getTAG()); //加入RequestQueue執行請求
        }else{
            //如果網路未開啟
            Log.i("Internet Error","user phone didn't open internet.");
        }

關於網路的判斷可以對照下面。

public boolean ifInternetOpen(Context context){

        final ConnectivityManager connMgr = (ConnectivityManager)
                this.getSystemService(Context.CONNECTIVITY_SERVICE); //呼叫App內建Service

        final android.net.NetworkInfo wifi =
                connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); //Wifi狀態

        final android.net.NetworkInfo mobile =
                connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); //3G、4G網路狀態

        if( wifi.isAvailable() || mobile.isAvailable()){
            return true;
        }
        else{
            Toast.makeText(context, "請開起網路連線" , Toast.LENGTH_LONG).show();
            return false;
        }
    }

之後在MainActivity就能很輕鬆的使用AppController.getInstance().GetKKTIXRequestToEventBean(MainActivity.this);載入網路上的KKTIX RSS的xml data。

撰寫解析KKTIX RSS的xml資料的映射物件

首先,觀察KKTIX RSS的xml data。網址如這,下面截圖已經取了片段說明。

XML

由上面說明撰寫物件。

xmlBeanList.java

public class xmlBeanList {

    @Root(name="feed",strict = false) //strict = false:非嚴格的feed tag中每個element和Attribute都要取,root為feed

        @ElementList(entry = "entry", inline = true) //<entry></entry>的list
        private List<xmlEventBean> beans; //集合,內容物對應到下面的xmlEventBean.java

        public xmlBeanList() {
        }

        public List<xmlEventBean> getMatches() { //必須要有GET method才能拿到data
            return beans;
        }
}

xmlEventBean.java

public class xmlEventBean {

    @Root(name="entry",strict = false) //root為entry

    @Element(name = "title", required = true) //<title>...</title>,required設true:沒符合的話會丟error
    private String title;

    @Element(name="link",required = true) //<link rel="..." ..... href="..."/>
    public class Link{
        @Attribute(name="href",required = true)
        private String url;
    }
    @Element(name = "summary", required = false) //<summary>...</summary>
    private String summary;

    @Element(name = "content", required = true) //<content>...</content>
    private String timeANDplace;

    @Element(name = "author", required = false)//<author>...</author>,對應到Author class
    private Author author;

 //必須要有GET method才能拿到data
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public String getTimeANDplace() {
        return timeANDplace;
    }

    public void setTimeANDplace(String timeANDplace) {
        this.timeANDplace = timeANDplace;
    }

    public Author getAuthor() {
        return author;
    }
}

Author.java

public class Author{

    @Element(name="name",required = true) 
    private String name;
    @Element(name="url",required = false)
    private String url;

    public String getName() {
        return name;
    }

    public String getUrl() {
        return url;
    }
}

最後將 xmlBeanList.class給SimpleXmlRequest class當參數,讓SimpleXmlRequest內的parseNetworkResponse中,serializer.read(clazz, data,false)去將XML data都解析成物件。其中的false代表XML格式不嚴格,要加才不易抱錯。

限制選項Enum: VolleyTAG

最後,為了配合不同的請求目標URL,就要有相應不同的TAG管理,Enum就是個很好限制選項的寫法。使用自訂Enum並 限制住呼叫建構子的方法,讓使用者不會搞錯TAG,詳細教學見這

public enum VolleyTAG {

   //限制住本class只允許2種建構子: VolleyTAG("KKTIX_ALL") / VolleyTAG("GOV_EVENT") 
    KKTIX_ALL("KKTIX_ALL"),
    GOV_EVENT("GOV_EVENT"); 

    private String TAG=null;

    VolleyTAG(String TAG) { 
        this.TAG=TAG;
    }

    public String getTAG(){
        return TAG;
    }
}

結果:成功映射出XML data物件

XML

程式碼

git clone下面專案到自己的電腦後,使用 git checkout a5ab .

SnapEvent
[SnapEvent]Android 5.0 的Toolbar+Tab+ViewPager

目標

本篇目標將解說如何用Android 5.0的Toolbar+Tab+ViewPager實作出以下效果。想看如何直接使用Github上神人開發的MaterialTabs套件,請拉至最下面。

Snapevent2 Image 02

Android 的Bar和Tab之發展歷程

自從Android 3.0 (API 11)出現ActionBar後,此原件被使用了很長一段時間,直到現在Android 5.0 的Material Design UI大變革,出現了取而代之的ToolBar!Tab撰寫方式五花八門,從使用Android 3.0後的Fragment實現Tab功能到Google釋出的開原專案-Sliding Tab(用ViewPager實作),製作滑動式標籤的開發者以此專案為基礎再客制化,更有人直接寫成套件讓大家開發更為便利! 之後Tab發展彷彿告一個段落,直到Android 5.0 降臨!好用的TabLayout結合Ripple套件的水波紋效果,讓App體驗更有質感,開發也變得更簡單了!

使用新技術時,最該注意的就是版本的向下支援!幸好Android 5.0的Material Design 有v7 appcompat library包和Theme.AppCompat主題能向下支援到API 7 (Android 2.X),包含到98%的使用者! 之前ActionBar 沒有支援Android 3.0 (API level 11) 以下的相容包,導致開源神人們自行開發出非官方的ActionBarSherlock向下支援到 Android 2.x ,讓開發者廣泛使用,直到官方相容包ActionBarCompat出世。 套件換來換去,真是苦了當時的Android開發者,各種不同版本的實作方法充斥網路也增加了Android初學者學習的困難度。所以如果向下相容包還未釋出,最好別太快使用新版本開發。

Android Support Library的包名,例如v4 Support Library、v7 gridlayout library、v7 appcompat library等,前面的v?代表向下支援到Android API Level ? 。

1 ) 引用程式包,使它能向下相容

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.android.support:design:22.2.1'
}

2 ) 建立ToolBar+Tab的xml布局

以前撰寫ActionBar要繼承ActionBarActivity再用getSupportActionBar()來取得控件,再進行進一步客製化。這種不直觀的撰寫方式,到ToolBar獲得改善。 有了ToolBar+TabLayout,讓上方導覽列也進入xml布局的行列,讓設計更有彈性。 建議將上方導覽列佈局分出成一個xml檔,在個別Activity佈局裡用include標籤匯入,才不會每次都Copy重複的程式碼,也增加程式的效率。

top_section.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"> #CoordinatorLayout為Android Design Support Library包的組件,為增強型FrameLayout

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> #作為Toolbar組件和TabLayout組件的Layout父容器,能設定Toolbar和TabLayout的共同屬性

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways"> #Toolbar我使用官方ThemeOverlay.AppCompat.Light style,只調整background顏色

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="SnapEvent"
                android:layout_gravity="center"
                android:id="@+id/toolbar_title"
                android:textSize="16dp"
                android:textColor="#ffffff" /> #添加Text在Toolbar中間

            </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="@style/MyCustomTabLayout"
            android:background="@color/window_background"/> #Tab使用自定義MyCustomTabLayout style

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

把上方導覽列匯入到main_activity.xml並在下方加上ViewPager控件,等待之後和TabLayout連結。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include layout="@layout/top_section"
     /> #include top_section.xml進來

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

如果要在ToolBar中新增menu表單,請參照此篇教學,作法和ActionBar相差不大。

3 ) 建立客製化的MyCustomTabLayout style

style.xml

<resources>
    <style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
        <item name="tabMaxWidth">@dimen/tab_max_width</item>
        <item name="tabIndicatorColor">#FA5858</item>
        <item name="tabIndicatorHeight">2dp</item>
        <item name="tabPaddingStart">12dp</item>
        <item name="tabPaddingEnd">12dp</item>
        <item name="tabBackground">?attr/selectableItemBackground</item>
        <item name="background">@android:color/white</item>
        <item name="tabTextAppearance">@style/MyCustomTextAppearance</item>
    </style> #tabTextAppearance引用到下方style

    <style name="MyCustomTextAppearance" parent="TextAppearance.Design.Tab">
        <item name="textAllCaps">false</item>  
    </style> #Tab的title是否全大寫,如果Tab上是放icon就設false

</resources>

你可能會看到Android studio 專案內,有value和value-v21(如果你的專案使用的SDK為API 21)兩個資料夾,裡面都有style.xml。 如果使用者手機的Android API level在21以上,App會使用value-v21資料夾內的style;反之使用value資料夾內的style。 所以兩個資料夾內,各自style.xml內的style name要相同,value-v21和value之間才有發揮作用。至於刪除value-v21資料夾不會影響專案。

4 ) 實作要給ViewPager的FragmentPagerAdapter的Fragment

這裡大家就各自發揮,下面只列最基本要override的method,別忘建立Fragment的xml佈局。

public class MyFragment extends Fragment {
   
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_page, container, false); //實體化佈局
        return view;
    }
}

4 ) 實作要給ViewPager的FragmentPagerAdapter

public class MainPageAdapter extends FragmentStatePagerAdapter {

    private int[] imageResId = {
            R.drawable.ic_search_black_24dp,
            R.drawable.ic_reorder_black_24dp,
    }; //要放在Tabs上的圖s

    private Context context;
    List<Fragment> fragments; //切換頁面的Fragments

    public MainPageAdapter(FragmentManager fm , List<Fragment> f,Context context) {
        super(fm);
        this.context=context;
        fragments=f;
    }

    @Override
    public int getCount() { //頁卡數量
        return fragments.size();
    }

    @Override
    public Fragment getItem(int position) { //回傳Frament頁卡
       return fragments.get(position); //從上方List<Fragment> fragments取得
    }

    @Override
    public CharSequence getPageTitle(int position) { //在此回傳Tab title string

    //目前TabLayout還沒有提供直接的方法放icon到Tab上,必須自行在getPageTitle實作

        Drawable image = context.getResources().getDrawable(imageResId[position]); //設定Tabs圖片
        image.setBounds(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
        SpannableString sb = new SpannableString(" ");
        ImageSpan imageSpan = new ImageSpan(image, ImageSpan.ALIGN_BOTTOM);
        sb.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return sb;
    }
}

想參考更多如何客製化TabLayout的title請至Google Play Style Tabs using TabLayout

4 ) 在Activity完成控鍵間和屬性等…的設定

public class MainActivity extends AppCompatActivity { //ActionBarActivity在Android 5.0已經被廢棄了...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity); //設定xml佈局

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

        toolbar.setLogo(R.mipmap.ic_launcher);  //左上方logo圖

        setSupportActionBar(toolbar); //讓支援ActionBar的method可以使用,使熟悉ActionBar的開發者能調用getSupportActionBar()作外觀設定。

        final ActionBar ab = getSupportActionBar();
        ab.setDisplayShowTitleEnabled(false);  //取消Toolbar的內建靠左title(像Actionbar的特性)

        //ab.setDisplayHomeAsUpEnabled(true);  回到上一個Activity時使用

        List<Fragment> fl=new ArrayList<Fragment>(); //填充要的Fragment頁卡
        fl.add(new MyFragment());
        fl.add(new MyFragment());

      ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
        if (viewPager != null) {
            viewPager.setAdapter(new MainPageAdapter(getSupportFragmentManager(), fl , MainActivity.this));  //設定Adapter給viewPager
        }

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.setupWithViewPager(viewPager);   //連結viewPager給TabLayout
    }
}

更多關於setSupportActionBar方法多說明請入內,長知識了!

4 ) 大功告成,成果如下

注意再import使用包時,引用v7、v4等…版本包比較能向下相容,用了後,整個專案的引用包都要保持一致,否則會報錯。

5 ) 程式碼

git clone下面專案到自己的電腦後,使用 git checkout 1e9b . ,還原到剛完成滑動Tab時的版本。

SnapEvent

使用Github上神人開發的MaterialTabs套件

Github上有很多神人提供已經包好的MaterialTabs套件,經過一段考慮後,我決定使用pizza/MaterialTabs。 我選擇它有幾點原因:

  1. 支援Android API 9 以上所有版本
  2. 套件版本已經到2.0.2,代表基本上的issue都有解決了
  3. 提供超好用App直接Tab style客製化!
  4. 有material-ripple的水波效果!炫

教學在repo的README.md說明得很清楚,這裡指大致提一下。

1 ) 下載它的App-Material Tabs Demo後,開啟App會看到以下畫面,準備客製化自己的Tab外觀。

觀察演示,滿意後把xml標籤檔寄給自己。

2 ) 在dependencies添加套件,把剛寄給自己的xml標籤添加到自己佈局的Toolbar和ViewPager之間,不用管TabLayout之類。

dependencies {
        compile 'io.karim:materialtabs:2.0.2'
    }
<include layout="@layout/mytoolbar_layout"/>
    <!--Use App MaterialTabs to customize the tab's appearance-->
    <io.karim.MaterialTabs
      ......
        />
    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

3 ) 讓MaterialTabs與ViewPager相連,別忘自己實作Fragment和FragmentPageAdapter給ViewPager

MaterialTabs tabs = (MaterialTabs) findViewById(R.id.material_tabs);  // Use materialTabs
tabs.setViewPager(viewPager);

4 ) 超快的完成了!

5 ) 程式碼

git clone下面專案到自己的電腦後,使用 git checkout practice ,切換到branch practice

SnapEvent

下次會是圖片緩存及用http請求server端資料等內容,繼續衝刺了!