2015年5月31日 星期日

Android 開發 (九十九) What's New in M support-design-widget NavigationView

NavigationView的使用情境是在側欄

套上navigationview 讓你可以較容易的刻出material的樣板
你只需要在layout裡放

<android.support.design.widget.NavigationView    android:id="@+id/navigation"    android:layout_width="wrap_content"    android:layout_height="match_parent"    android:layout_gravity="start"    app:headerLayout="@layout/headerlayout"    app:menu="@menu/menu_main" />

headerlayout為上方的板塊,headerlayout 可以利用addHeaderView
增加header 剩餘下方的選項,如上方的settings 則是寫在menu裡
利用menu做設定,這種設計的作法,感覺google也是想告訴我們
側欄應該只放設定相關的東西,而不應該放太多有的沒的資訊吧.

Android 開發 (九十八) android studio 1.13 databinding part2

上一篇有提到,databinding的介紹,今天這篇要介紹的是,
假設model變更時,要如何讓view也跟著改變呢?

其實方法很簡單,android 有提供新的class BaseObservable
如下圖

    public class User extends BaseObservable {
        private String firstName;
        private String lastName;

        @Bindable
        public String getFirstName() {
            return firstName;
        }

        @Bindable
        public String getLastName() {
            return lastName;
        }

        public User(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
            notifyPropertyChanged(BR.firstName);
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
            notifyPropertyChanged(BR.lastName);
        }
    }

我們只需要在set之後,call notifyPropertyChanged就行了

    TwowaydatabindingLayoutBinding binding;
    User user;
    private int i = 0;

    @Overrideprotected
    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.twowaydatabinding_layout);
        user = new User("Test", "User");
        binding.setUser(user);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                user.setLastName("change " + i);
                Toast.makeText(view.getContext(), " " + user.getLastName(), Toast.LENGTH_SHORT).show();
                i++;
            }
        });
    }
如上面的code所示,我在按鈕click的時候去改變user的lastName
然後edittext的lastname就會跟著改變

其實這種方法很像是adapter的notifydatasetChanged
不過目前這種方法只能用在 data change 通知 ui 改變
但是 ui 改變 -> data change這條路似乎還沒有看到任何的解法

希望在 android M正式release 的時候可以有 two-way binding的寫法出現.

2015年5月30日 星期六

Android 開發 (九十七) android studio 1.13 databinding part1

databinding 之前在寫windows的時候就用過,由於非常好用,
所以一直期待android 也可以support,如今android studio 1.13 support了這個功能!!

現在就來介紹如何使用

首先必須先將android studio升級到1.13
接下來必須在專案的 build.gradle裡加入
classpath 'com.android.databinding:dataBinder:1.0-rc0'

接下來必須在app的build.gradle裡加入
apply plugin: 'com.android.databinding'

然後就可以在app裡使用databinding的功能了

<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">    <data>        <variable name="user" type="com.designsupportlibrary.normaldatabinding.NormalUser"/>    </data>    <LinearLayout        android:orientation="vertical"        android:layout_width="match_parent"        android:layout_height="match_parent">        <EditText android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="@{user.firstName}"/>        <EditText android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="@{user.lastName}"/>        <Button            android:id="@+id/btn"            android:layout_width="wrap_content"            android:layout_height="wrap_content" />    </LinearLayout></layout>

如上面的xml file data代表著要連動的model
這代表著我們有一個 user class

public class NormalUser  {
    private String firstName;    private String lastName;
    public String getFirstName() {
        return firstName;    }
    public String getLastName() {
        return lastName;    }

    public NormalUser(String firstName, String lastName) {
        this.firstName = firstName;        this.lastName = lastName;    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

接著我們只需要將normalUser 與 ui 做綁定的動作即可
    NormalMainBinding binding;
    NormalUser user;

    @Overrideprotected
    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.normal_main);
        user = new NormalUser("Test", "User");
        binding.setUser(user);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(view.getContext(), user.getLastName(), Toast.LENGTH_SHORT).show();
            }
        });
    }

所以這邊代表的意思就是,
首先我們會create一個binding的物件,他與view做綁定,但是我們還沒有將data餵給他
接著我們init User ,然後使用setUser 將 view 與 資料綁定

這時候 上方的edittext 就會去取得 firstName的資料
而下方的edittext就會去取得 lastName的資料,

不過目前的綁定方法就只有set下去的瞬間而已,之後你再對user做任何的改變都不會有其他相對應的變化了,如果要有相對應的變化,那就必須針對User做額外的動作,
不過這個就得等到part2 的時候再說囉XD

2015年5月29日 星期五

Android 開發 (九十六) What's New in M support-design-widget TabLayout

先讓我們看一下圖



在TabLayout出來之前,如果要做出類似的功能,除了自己刻之外,我想大部份的人都會利用https://github.com/astuetz/PagerSlidingTabStrip這個library吧

這個TabLayout 讓我們可以使用官方的api 而不用再依照3-party lib,不用再擔心library不再維護的問題了XD

接下來介紹該如何實做

跟正常的viewpager and tab一樣必須定義這兩個layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

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

接下來只需要initViewpager
ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);setupViewPager(viewPager);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);

然後再使用 setupWithViewPager 將viewpager set 進去就行了,
這邊有一個設定要特別注意, setTabMode 有分 MODE_FIX and MODE_SCROLLABLE
其中如果使用MODE_SCROLLABLE  tab的textview的寬度才會正常顯示,否則textview的寬度會只有一個字的寬度

如上圖所示

不過在使用這個api的時候有遇到一個問題,當我利用下面的程式碼
tabLayout.setTabTextColors(Color.BLUE,Color.parseColor("#FF2750"));
想要更換tab的顏色時,卻無法正確更換,
當然還有下面那條線的顏色,目前還找不到更換的方法,
我猜測只能由theme來更換了. 如果有試出來再跟大家分享囉

Android 開發 (九十五) What's New in M support-design-widget Snackbar

What's SnackBar ? 讓我們看一下下面的圖片

SnackBar我們可以理解為進階版的toast
例如說當用戶刪除了某項商品,我們其實無法用比較不擾人的方式
去提示用戶是否要刪除該商品

如果用dialog 非常擾人
如果用toast 非常無感...而且無法做任何undo的動作

SnackBar應該是比較折衷的方式,利用show出一小段提示,讓用戶有機會可以反悔

講了那麼多我們要來介紹來如何實做了
要使用SnackBar我們只需要
    findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
        @Override public void onClick (View view){
            SpannableString s = new SpannableString("you just remove this product");
            s.setSpan(new ForegroundColorSpan(Color.RED), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            Snackbar.make(view, s, Snackbar.LENGTH_SHORT).setAction("undo", new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Log.d("Ted", "do click");
                }
            }).setActionTextColor(Color.parseColor("#FF2750")).show();
        }
    });

如上使用 Snackbar.make(...).show()就可以了,比較跟toast不同的地方在於,
snackbar需要view,而且文字可以換色,並且擁有click event,

在這邊還有一件事情要特別提到,Snackbar並沒有提供設定左邊文字顏色的api,
不過我們可以利用SpannableString來換色,如上面的sample code

我想之後很多app都會使用到這樣的特效吧. 個人真的認為還蠻實用的!!

Android 開發 (九十四) What's New in M support-design-widget FloatingActionButton

what's FloatingActionButton? 讓我們來看看下面的圖



值得注意的是我的版本是4.4 ,但是有shadow,而且點擊的時候也有shadow,
接下來我們要介紹如何實作,FloatingActionButton的實作方式更簡單
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:android.support.design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:src="@drawable/abc_ic_clear_mtrl_alpha"
        android.support.design:fabSize="mini" />

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:src="@drawable/abc_ic_clear_mtrl_alpha"
        android.support.design:fabSize="normal" />
</LinearLayout>

只需要將ui放到xml上就完成了XD
在這邊值得一提的是,floatingActionButton有一個property fabSize,
要在xml上使用則必須加上
xmlns:android.support.design="http://schemas.android.com/apk/res-auto"
才能正確使用,fabSize有分normal and mini如上圖,就是正常size和mini size

今天介紹floatingActionButton就到這邊,接下來還會有需多新的ui會介紹唷!!

Android 開發 (九十三) What's New in M support-design-widget TextInputLayout

google io 2015 推出了新的support lib.
其中包含了 'com.android.support:design:22.2.0'
這個lib多了許多實用的material ui ,最近會一一介紹

今天要介紹的是 TextInputLayout



如上圖當沒有focus而且沒有key字的時候,hint會顯示在edittext裡,
當focus時hint就會像是title一樣顯示在edittext上方,

我想上面的圖片很清楚的展示了他的效果

至於要如何實做?

首先必須在gradle裡加入
compile 'com.android.support:design:22.2.0'

接著在xml裡加入textinputlayout
<android.support.design.widget.TextInputLayout    android:id="@+id/inputlayout"    android:hint="name"    android:layout_width="wrap_content"    android:layout_height="wrap_content" >    <EditText        android:hint="name"        android:layout_width="wrap_content"        android:layout_height="wrap_content" /></android.support.design.widget.TextInputLayout>

記得要在裡面放一個EditText,然後目前的TextInputLayout有一些問題
除非在TextInputLayout 和 EditText 加上hint 還有在code裡面也加上hint
TextInputLayout inputLayout = (TextInputLayout)findViewById(R.id.inputlayout);inputLayout.setHint("name");

否則上面的效果不會出現(至少在我的模擬器上面是這樣子的 genymotion 4.4)
只需要簡單的步驟,我們就設定好了,是不是很簡單!!

2015年5月28日 星期四

Android 開發 (九十二) JobInfo

什麼是JobInfo? 其實可以把它想成進階版的alarmManager
例如我想要每天trigger一次,但是只有在手機充電的時候才會trigger
使用alarmManager其實沒有辦法做到,but with JobInfo you can

here is sample code

ComponentName serviceName = new ComponentName(this, MyJobService.class);JobInfo jobInfo = new JobInfo.Builder(JOB_ID, serviceName)
        .setPeriodic(10000)
        .setRequiresCharging(true)
        .build();JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);int result = scheduler.schedule(jobInfo);

意思是每十秒會trigger一次 MyJobService, 但是只有在充電的情況下

public class MyJobService extends JobService {


    @Override    public boolean onStartJob(JobParameters jobParameters) {
        //do something        Log.d("Ted","startJob");                return true;    }
    
}

上面的sample 會造成每十秒trigger 一次  MyJobService

JobInfo其實算是蠻方便的功能,可惜只能在api21才能使用.

最後還是要附上sample code
https://github.com/nightbear1009/JobInfoSample

2015年5月27日 星期三

Android 開發 (九十一) Android L notification heads-up

今天要介紹如何在android裡添加 heads-up notification

主要的程式碼為  setFullScreenIntent
將 flag設為true就會出現heads-up notification了
如下
Notification notification = new Notification.Builder(MainActivity.this)
.setSmallIcon(R.drawable.ic_launcher)
.setFullScreenIntent(contentIntent, true)
.addAction(R.drawable.ic_launcher,"add",contentIntent)
.build();

在產生heads-up notification之後,接下來就是要研究如何設定btn的layout了
其實很簡單,我們只需要使用addAction即可,
第一個參數為icon ,第二個為wording,第三個為click之後的行為

如上圖,就是上面sample code產生的notification.

詳細的程式碼可以參考下面的連結
https://gist.github.com/nightbear1009/dd8d5a02f9dd566888c0

2015年5月23日 星期六

Android 開發 (九十) crashlytics 3.0 intro. Fabric

最近使用的crashlytics更新了,並且更名為Fabric,為了要跟上流行,所以就立刻將我的app做了升級的測試,其實主要並沒有太大的變更,不過這次主要要特別介紹兩個功能,
或許是舊有的,也可能是新的,小弟已經許久沒去follow他的文件

第一個功能是,可以紀錄crash的user資料,從前我們crash常常很難複製,其中一個原因就是我們不曉得使用者是誰,通常crash的人只會在googleplay上面寫




基於他們非常有用的提供協助給予我們線索,我想我們可以很快的解決掉這個問題

沒錯,現實是殘酷的,user絕對不可能會告訴我們他是在什麼樣的情境,什麼樣的畫面死掉
你只會看到他留下了一顆星加上不理性評語,所以怎麼辦,我們要想辦法在crash前留下更多的線索

而crashlytics提供了我們這樣的功能

        Crashlytics.setUserIdentifier("234489032");
        Crashlytics.setUserName("ted");
        Crashlytics.setUserEmail("123@google.com");

在app crash的時候就會將user資訊帶到他們的server去


這樣我們就又多了一項資訊可以判斷是否跟特定user有關

另一個功能我覺得也是很實用的功能,不知道大家是否也有以下類似的問題



app並不會crash但是user反應系統異常,這個問題要怎麼重現我也煩惱了很久
不過就在我看到了crashlytics的另一個功能之後,所有問題都迎刃而解啊

首先app不會crash最主要的原因一定是我們有做try catch,或者是額外的if else 處理..
所以我們就可以在那些判斷的地方將crashlytics的log功能加入

        try {
            throw new Exception();
        } catch (Exception e) {
            Crashlytics.log("something more info");
            Crashlytics.logException(e)
        }

如上,我們可以將更多的訊息帶入log 並且 使用 logException
這樣我們就可以在crashlytics看到更多相關的問題



這樣我們就有足夠的線索可以解決這些bug了

不過目前crashlytics還有一個很大的問題,就是有時候我們無法很確切地知道是死在哪個畫面,當下的view hierarchy,希望不久後會有相關的library能幫助我們提供相關的訊息啊

Android 開發 (八十九) facebook deeplink

相信有在開發app的大家,在facebook 上使用deeplink時,常常會遇到改版後就功能不正常的問題,由於最近我也遇到了類似的問題,所以就索性研究了facebook 的 document

deeplink 就是利用一個特定的連結,http://ted/123
然後假設user點擊該連結,如果該user有安裝 app則會開啟我的app
如果user沒有安裝app就會前往googleplay

今天要來介紹如何達到facebook 的 deeplink

目前facebook 有提供兩種 deeplink 的方式
第一種是你有特定的網站,特定的展示頁面
第二種是你沒有特定的網站,你只想create特定的連結

第一種是你有特定的網站,特定的展示頁面

例如下面的網站
http://tw.91mai.com/SalePage/Index/754680
這是一個銷售商品的頁面,我希望user在手機前往該頁面時會導向我手機版相對應的頁面
這時候我就可以使用第一種方法

方法其實很簡單,你只需要在網頁上加上 facebook 指定的meta即可

<head>
   
    <meta property="al:android:url" content="com.mypackage://story/1234">
    <meta property="al:android:package" content="com.mypackage">
    <meta property="al:android:app_name" content="myappName">
    <meta property="og:title" content="title" />
    <meta property="og:type" content="website" />
    
</head>
其中url 為 app要處理的資料,利用該資料就可以知道該user是在哪個頁面,我們就可以將app導向特定的頁面

package name就是app的 package name 這一個一定要跟app相同,否則會不work
app_name經過我的測試,隨便寫都可以
og:title 和 og:type 似乎是 必填欄位,如果沒特定需求就照著填就好了
更多相關的設定可以參考 http://applinks.org/documentation/


接著只要在app內加入這樣的程式碼就可以囉
<activity android:name=".MainActivity"
    android:label="@string/app_name" >
    ...
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="com.mypackage" />
    </intent-filter>
</activity>

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    FacebookSdk.sdkInitialize(this); 
    ...
    Uri targetUrl =
      AppLinks.getTargetUrlFromInboundIntent(this, getIntent());
    if (targetUrl != null) {
        Log.i("Activity", "App Link Target URL: " + targetUrl.toString());
    }
}
targetUrl就是特定的網址,我們就可以利用該url判斷應該導到app的哪個頁面


第二種是你沒有特定的網站,你只想create特定的連結

首先根據facebook 文件
你可以將你需要的參數填入,
其中最重要的是  url  你想帶入app的網址
package 你要開啟的app
curl https://graph.facebook.com/app/app_link_hosts \
-F access_token="APP_ACCESS_TOKEN" \
-F name="Android App Link Object Example" \
-F android=' [
    {
      "url" : "sharesample://story/1234",
      "package" : "com.facebook.samples.sharesample",
      "app_name" : "ShareSample",
    },
  ]' \
-F web=' {
    "should_fallback" : false,
  }'
接著你會得到一組id
{"id":"643402985734299"}
接著在利用下方網址 將上面的id 帶入
curl -G https://graph.facebook.com/643402985734299 \-d access_token="APP_ACCESS_TOKEN" \
-d fields=canonical_url \
-d pretty=true
接著會得到一組網址
{
   "canonical_url": "https://fb.me/643402985734299",
   "id": "643402985734299"
}
這個網址就是我們創造出來的網址,我們可以將這個網址貼到facebook上,只要有人點擊該link 有裝app 就會前往我們的app,沒有app的就會被導向googleplay

當然app還是要依照第一種方法做設定,這樣才app才可以攔截到該url,並且導到我們希望的頁面





Android 開發 (八十八) 使用AndroidJUnitRunner


android最近新增了

AndroidJUnitRunner


  • 能夠run JUnit3 and JUnit4的 test
  • 能夠做test filtering SmallTest MediumTest LargeTest
  • Activity /Application Life cycle Monitoring
今天會介紹如何使用AndroidJUnitRunner寫test case
defaultConfig {
        applicationId "com.androidtestcase"
        minSdkVersion 14
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

android {
    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.0.0'
    androidTestCompile 'com.android.support.test:runner:0.2'
    androidTestCompile 'com.android.support.test:rules:0.2'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'
}
主要要加的地方有 packagingOptions那段
還有androidTestCompile那些code
接著就寫一個簡單的test吧,如下

@RunWith(AndroidJUnit4.class)
@SmallTest
public class testMyMethod {
    @Test
    public void test(){
        MyMethod m = new MyMethod();
        Assert.assertEquals(m.getData(),"data1");
    }
}


你只需這樣寫就能夠寫出一個簡單的測試
其中 SmallTest代表test 的size ,目前有分 small / medium/ large
想要測試smallTest 你可以先使用以下cmd


adb shell pm list instrumentation
他會列出test list 如下
instrumentation:com.androidtestcase.test/android.support.test.runner.AndroidJUnitRunner (target=com.androidtestcase)

例如我的package name為 com.androidtestcase 則我必須下cmd
adb shell am instrument -w -e size small com.androidtestcase.test/android.support.test.runner.AndroidJUnitRunner

這樣就只會測small的test case
可以依照需求,測試我想測的case


接著下面介紹了如何取得test case 的report

在console下command

./gradlew connectedAndroidTest
就可以得到test 的xml格式report



在console下command
./gradlew test
就可以得到html格式的report


xml格式的report比較適合在CI裡面使用,html格式的比較適合個人觀看用


結論
目前我使用新的AndroidJUnitRunner純粹是為了可以filter test size
當我只想做快速的unit test 的時候就不再需要重新跑一次所有的 ui test 囉!!

2015年5月19日 星期二

Android 開發(八十七) android studio 1.2

不知道大家是否都已經更新了android studio 1.2,
1.2多了很多新功能,


以前debug的時候總是要花很多時間去看參數的數值是什麼,
更新了1.2之後,當你在step by step debug時,參數都幫你寫在後面了,
真的是蠻方便的,不過最方便的不是這個,而是


以前if 裡面的判斷式總是要自己去看數值來判斷,如今他會直接幫你寫在上面,
真是貼心的設計啊!!!

還新增了CPU監測的功能

不過小弟目前也還不太知道要怎麼用這個功能,感覺是炫技(?!)
當然還有新的unit test 不過這部分小弟尚未研究到,等小弟研究完再一一跟大家介紹囉

2015年5月12日 星期二

Android 開發(八十六) Regex regular expression

RegularExpression應該是所有開發者,遲到會遇到的東西,
第一次遇到的時候花了我好多時間才理解要怎麼操作,

先講幾個常用的符號

*  代表字串中有0 ~無數個前一符號

假設給定12*
則Match的會是 1 , 12 , 122
不Match的會是 123


+ 代表字串中有1 ~ 無數個前一符號

假設給定12+
則Match的會是  12, 122
不Match的會是  1, 123

[] 代表字串中有[...]內的文字

假設給定[a-z]
則Match的會是 a , b ,c ....z
不Match的會是 A, 1, &

從上面幾個符號的組合舉個例子

給定[a-zA-Z0-9]+  
則Match的會是fkjlajklj , kjackjl , vnma
不Match的會是  !, @#$, $^^

例如要判斷手機號碼則   [0-9]{10} 判斷必須符合十個數字

如果要再進階一點的話   ^09[0-9]{8}  判斷必須前兩個數字為09開頭

除此之外,還有一個比較特別的用法

                Pattern p = Pattern.compile("push\\('([\\u4e00-\\u9fa5/]+)'\\));
                Matcher m = p.matcher(parseData);
                while (m.find()){
                    Log.d("Ted","group "+m.group(1));
                }

其中[\\u4e00-\\u9fa5/]代表著所有的中文符號

當 parseData = "push('文字')"  則  m.group(1)得到的就會是     文字  
利用這種方式就可以將match的特定文字取出來,
非常方便的用法啊!!