EDIT MODE

モバイルアプリ開発に関することを書いています

JRebel for Android betaを試してみました

JRebel for Android とは?

ZeroTurnaround社のプロダクトの一つで、Androidアプリ開発を爆速にしようとするツールのようです。キャッチコピーは、Live Android Developmentです。

今はまだベータ版で、招待制になっています。

導入方法

注意

invitationを受け取ったメールアドレスでactivateしないと、このツールは使えません。 使ってみたい場合は、この公式サイトからbeta版を申し込みましょう。

1. Pluginのインストール

IntelliJ IDEA pluginとして提供されているので、AndroidStudioのPreference > Pluginからインストール出来ます。今回は限定的な公開なので、専用のリポジトリを指定してそこからインストールしました。

AndroidStudioを再起動すると、Activateするためのダイアログが表示され、招待されたメールアドレスを入力すると以下のようなダイアログが出て、インストールが完了しました。

f:id:androhi:20150618020812p:plain

2. Run with JRebel for Androidを実行

とりあえずJRebel for Androidが追加したRunボタンがあるから、それを実行しろとのことなので、適当なプロジェクトを開きました。確かにそのようなボタンが増えてました。ロケットですね。

f:id:androhi:20150618020827p:plain

最初にこのRun with JRebel for Androidを実行すると、何かイニシャライズしてるのか分かりませんが、通常のGradleでのビルドよりちょっと時間がかかりました。

初回のビルドが終わると、自動で以下のようにbuild.gradleにpluginの記述が追記され、専用のコンソールがオープンされました。

apply plugin: 'com.zeroturnaround.jrebel.android'

f:id:androhi:20150618020846p:plain

使い方

1. Run with JRebel for Android

準備として、必ず最初にRun with JRebel for Androidで実機にアプリを、インストール&起動させる必要があるようです。そうするとJRebel for Androidのコンソールに、"Application started and ready for use." と出ます。

同じく実機の方にも、"JRebel for Android started" のToastが表示されました。

f:id:androhi:20150618020905p:plain

2. Make Module

1の状態を保ったまま、ソースコードなりリソースなり修正します。修正が終わったら、Build > Make Module 'app' を実行します。ショートカットだと shift+command+F9です。

しばらくするとアプリの表示が更新され、"Reloaded classes" とまたもやToastが表示されました。

f:id:androhi:20150618020924p:plain

感想

試した環境はこんな感じです。

  • MacBookAir Mid 2011
  • AndroidStudio 1.3 Preview3
  • Xperia Z1f
  • サンプルコード程度のかなり小さいプロジェクト

5回ほど更新してみたところ、だいたい10秒前後で実機に反映されました。 確かにこれなら実機で爆速開発が出来そうな気がしました。特にリソースの変更をすぐに実機で確認したいとか、よくあるケースなので神ツールになりそうな感じがします。

ちなみに、実機側でも "Service connected" とToastが出るので、何かServiceが動いてるようです。が、どれがJRebelのかは分かりませんでした。

Design Support Library v22.2.0について Part 3

Part 1ではTextInputLayoutとFloatingActionButtonについて、Part 2ではSnackbarとCoordinatorLayoutについて調べたことを、それぞれ書きました。

今回は、TabLayoutについて書いていきます。

TabLayoutの概要

リファレンスを確認すると、TabLayoutクラスはHorizontalScrollViewクラスを継承していました。 API Level 20まで使われていたActionBar.Tabなどとは、関連がないようです。 Tabの生成にはTabLayout.TabクラスnewTab()メソッドを使います。

MaterialDesignにおけるTabの使い方は、デザインガイドラインで細かく指定されています。 このクラスの見た目や振る舞いは、ガイドラインに沿った形で実装されているようです。

基本的な使い方

とりあえずTabを画面に配置するために、各Tabのコンテンツは一旦無視して、以下のように実装してみました。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</RelativeLayout>
public class MainActivity extends AppCompatActivity {

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

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.addTab(tabLayout.newTab().setText("tab 1"));
        tabLayout.addTab(tabLayout.newTab().setText("tab 2"));
        tabLayout.addTab(tabLayout.newTab().setText("tab 3"));
    }
}

f:id:androhi:20150617014729p:plain

やっていることは、newTab()メソッドでタブを生成してsetText()メソッドでタブのタイトルを設定しているだけですが、デフォルトでも以下のような点が実現出来てました。

  • タブのタイトルが自動でAllCaps
  • タブのインジケーターがアニメーションする
  • カレントでないタブのタイトル色が薄くなる

またsetIcon()メソッドでアイコンをタブに表示することも出来ます。アイコンサイズは24dpです。

tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_event_grey_600_24dp));
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_devices_grey_600_24dp));
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_directions_subway_grey_600_24dp));

f:id:androhi:20150617014754p:plain

ただし、setText()setIcon()を両方使うと横並びに表示されてしまい、ガイドラインTabs with icons and textとは異なってしまいました。この場合は、setCustomView()メソッドガイドラインに沿うよう、補うしかなさそうです。

f:id:androhi:20150617014814p:plain

ModeとGravity

ガイドラインを見ると、Tabには以下のような2つのModeと2種類の見た目について、記述されています。

  • Mode : Fixed / Scrollable
  • Gravity : Center / Fill

それぞれsetTabMode()メソッドsetTabGravity()メソッドで指定出来ました。

1. Fixed & Center

f:id:androhi:20150617014835p:plain

2. Fixed & Fill

f:id:androhi:20150617014858p:plain

3. Scrollable & Fill

f:id:androhi:20150617014921p:plain

4. Scrollable & Center

f:id:androhi:20150617014936p:plain

ViewPagerとの併用

リファレンスにも書かれているように、TabLayoutクラスはViewPagerと併用するためのインターフェースやメソッドが用意されています。 個人的には、これがTabLayoutクラスを使う醍醐味だと感じました。

セットする方法は2パターン用意されていて、簡単に言うとマニュアル方式とオートマチック方式です。 マニュアル方式だと、以下の3ステップで設定します。

  1. TabLayout#setTabsFromPagerAdapter(PagerAdapter)メソッドでPagerAdapterをTabLayoutにセットする
  2. TabLayout.TabLayoutOnPageChangeListenerインターフェースでTabの切り替えイベントをViewPagerにリンクする
  3. TabLayout.ViewPagerOnTabSelectedListenerインターフェースでViewPagerの切り替えイベントをTabLayoutにリンクする

オートマチック方式では、上記3ステップと同等の処理を1つのメソッド(TabLayout#setupWithViewPager(ViewPager))の内部で、全てやってくれるそうです。

ちなみにどちらの方式も、タブタイトルは内部でPagerAdapter#getPageTitle(int)メソッドから取ってきたものを、セットするとのことです。

これらを踏まえてコードで書くと、以下のようになりました。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <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/pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tabs"
        />

</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/page_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="32sp"
        />

</RelativeLayout>
public class MainActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener {

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

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        ViewPager viewPager = (ViewPager) findViewById(R.id.pager);

        FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return TestFragment.newInstance(position + 1);
            }

            @Override
            public CharSequence getPageTitle(int position) {
                return "tab " + (position + 1);
            }

            @Override
            public int getCount() {
                return 3;
            }
        };

        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(this);

        //オートマチック方式: これだけで両方syncする
        tabLayout.setupWithViewPager(viewPager);

        //マニュアル方式: これでViewPagerのPositionとTabのPositionをsyncさせるらしい
        //tabLayout.setTabsFromPagerAdapter(adapter);
        //viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        //tabLayout.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager));

    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        Log.d("MainActivity", "onPageSelected() position="+position);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    public static class TestFragment extends Fragment {

        public TestFragment() {
        }

        public static TestFragment newInstance(int page) {
            Bundle args = new Bundle();
            args.putInt("page", page);
            TestFragment fragment = new TestFragment();
            fragment.setArguments(args);
            return fragment;
        }

        @Nullable
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            int page = getArguments().getInt("page", 0);
            View view = inflater.inflate(R.layout.fragment_test, container, false);
            ((TextView) view.findViewById(R.id.page_text)).setText("Page " + page);
            return view;
        }
    }
}

f:id:androhi:20150617015000p:plain

もしタブタイトルにアイコンを設定したい場合は、getPageTitle()でnullなどを返してTabLayoutにViewPagerをセットした後で、setIcon()を使えば良さそうです。

先程のコードを、以下のように書き換えました。(変更がない箇所は省略しています)

...
@Override
public CharSequence getPageTitle(int position) {
    return null;
}

...
        
//オートマチック方式: これだけで両方syncする
tabLayout.setupWithViewPager(viewPager);

//アイコンセット
tabLayout.getTabAt(0).setIcon(R.drawable.ic_devices_grey_600_24dp);
tabLayout.getTabAt(1).setIcon(R.drawable.ic_directions_subway_grey_600_24dp);
tabLayout.getTabAt(2).setIcon(R.drawable.ic_event_grey_600_24dp);
...

f:id:androhi:20150617015019p:plain

ブランドカラーの設定

以下のようにブランドカラーを設定する場合は、ほぼToolbarと同じ設定の仕方でした。

DarkかLiteかは親のテーマを引き継ぎますし、背景色はandroid:background=?attr/colorPrimaryの記述が必要です。 インジケーターには特に何も指定しなくてもcolorAccentが適用されるようです。

もしタブタイトルのテキスト色を変えたい場合は、setTabTextColors(ColorStateList)メソッドsetTabTextColors(int, int)メソッドを使って変更出来ます。

<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
    <item name="colorPrimary">#3f51b5</item>
    <item name="colorPrimaryDark">#303f9f</item>
    <item name="colorAccent">#ff4081</item>
</style>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/toolbar"
        android:background="?attr/colorPrimary"
        />

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tabs"
        />

</RelativeLayout>

f:id:androhi:20150617015046p:plain

まとめ

TabLayoutいいクラスですね。タブの実装が、かなり楽になりそうだなと思いました。 特にViewPagerと使うことを十分に考慮してあるのが、とても助かります。

ただタブタイトルを自動でgetPageTitle()からセット出来たりするとこまでやってくれるのであれば、いっそTabPagerViewクラスとか作ってしまってsetTabEnable(boolean)メソッドとかでタブが生えたり消えたり出来たら良かったなーとも思いました。

とはいえ、ほとんど場合は今まで通りViewPagerを生成して、TabLayout#setupWithViewPager(PagerAdapter)を呼べば済みそうなので、本当に便利なクラスだと思います。

世界に羽ばたく!! Product Manager Night に参加してきました

TL;DR

以下、イベントページからの抜粋です。

シリコンバレー界隈では「Engineering BackgroundをもったProductManagerが重要だ」 と言われEngineerのNext StepとしてのProduct Managerが定着してきているように思います。 日本においてもそういったProduct Managerが増えてきてるのではないでしょうか?

エンジニアからProductManagerになるといいこと多いし、すごくやりがいのある職種だよという話しが盛り沢山のイベントでした。

概要

※ 資料は近日公開らしいです(たぶんイベントページに掲載されるはず)

今回勉強になったこと

4名の方が発表されたのですが、それぞれ経験則に基づいたProductManager論をまとめていて、どなたの発表もとても面白かったです。 資料は公開されるそうなので、細かい部分はそれを見たほうが参考になると思うので、この記事では個人的に勉強になったことをまとめようと思います。

ProductManagerの役割について

発表者の共通的な意見として

  • とにかくProductに関わるもの(チーム/開発/ビジネス)は全てやる
  • それらを成功に導く

といったことが、大枠での役割とのことです。 ただし、所属企業の規模や事業内容、または個人のスキルセットなどで変わってくるので、一概にこれというものも無いようです。

とはいえ、みなさん口を揃えておっしゃってたのが、「誰よりもプロダクトを理解し、プロダクトを愛して、それを周りに伝えること」が最も重要な役割だということです。

エンジニアがなるProductManagerになるメリット

エンジニアリングをバックグラウンドに持っていることによって技術的な判断が加味されて、いろいろな決断の精度が上がったり、開発工程にも積極的にコミット出来るという強みが生まれること。 という感じでした。

あとは純粋に「エンジニアは技術が分かるリーダーを好むよね」という話しも。

エンジニアだったが故のデメリット

まとめると、自分基準になってしまうことに尽きるようです。 これはスケジュールや技術的な判断など、どうしても自分の経験から導き出してしまう弊害があるようです。

例えば、この開発は(俺だったら)3日くらいかな、こういう要件の実装は(俺だったら)ちょっと難しそう、などといった無意識な「俺だったら」が出てしまうんだそうです。

ProductManagerってどうなの?

  • とにかくやること多くて忙しい
  • 市場やサービスを作ってる感のやりがいは半端ない
  • エンジニアこそなるべき

具体的には、チームマネジメントもプロダクトのマネジメントも、全てが技術的な理解が欠かせないので、そういったことに長けてるエンジニアがProductManagerになることのメリット多いとのことです。

まとめ

ちょうど先日メルカリさんの記事にも、似たような話しが出てきて気になっていたので、個人的にもいろいろ考えていきたいなと思えるようになりました。

エンジニアのキャリアアップって、いろんな方向性があって本当に難しいと思います。その中の一つの道としてProductManagerについて勉強できたので、このイベントに参加してよかったです。

登壇者の方、主催のスマートニュースの方には、よいイベントを開いて下さって本当に感謝しています。ありがとうございました! 特にエンジニアからProductManagerにキャリアアップされた方の生の声が聞けたことで、具体的なお話がとても参考になりました。

Design Support Library v22.2.0について Part 2

前回の記事では、TextInputLayoutとFloatingActionButtonについて書きました。 androhi.hatenablog.com

今回は、主にSnackbarとCoordinatorLayoutについて調べたことを、書いていきます。

SnackBar

Android 4.4まではアプリ内で簡易的な通知を行う際に、Toastを使った実装が多かったと思いますが、Android 5.0以降はSnackbarと呼ばれる実装を使うよう推奨されてます。

UIとしてはToastととてもよく似ていますが、Snackbarにはアクションを含めることが出来る点や、スワイプ操作で表示を消すことが出来る点などが異なっています。

詳細は、公式サイトのSnackbars&toastsが詳しいです。

Snackbarの表示

使い方はほぼToastと同じなので、以下のようなコードで表示させてみました。

final LinearLayout layout = (LinearLayout) findViewById(R.id.root_layout);
Snackbar.make(layout, "Snackbar test", Snackbar.LENGTH_LONG)
        .setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .show();

f:id:androhi:20150603021218p:plain

注意しなければいけないのは、make()メソッドの第一引数にSnackbarをホールドするための親となるViewを渡す必要があることです。 アクションを含めたいときは、setAction()メソッドにラベルとリスナーを渡すだけでした。

なお一つのSnackbarにアクションは一つしか含めることが出来ません。 メッセージは文字数が多い場合は、適時改行されます。 試しに以下のコードを実行すると、下図のようになりました。

final LinearLayout layout = (LinearLayout) findViewById(R.id.root_layout);
String message = "long test message. long test message. long test message. long test message.";
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
        .setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .setAction("REDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .show();

f:id:androhi:20150603021241p:plain

CoordinatorLayout

このクラスはMaterialDesignならではだなぁという感じのViewGroupです。あるViewがスクロールした際に、連動して他のViewもスクロールするようなUIを作るために使用するクラスのようです。GoogleのInboxアプリでよく見ますね。

Support Libraryのリリースノートによると、Design Libraryのコンポーネントの多くは、このCoordinatorLayoutの子Viewになることを当てにするとあります。

Class Overviewでは、主なユースケースとして次の2点を挙げています。

  1. As a top-level application decor or chrome layout
  2. As a container for a specific interaction with one or more child views

要約すると最初に書いたように、特別な依存関係を構築したいViewの親ViewGroupとして使えということのようです。

CoordinatorLayout + FloatingActionBar + Snackbar

分かり易い例として、Snackbarが表示されるとそれに合わせてFABもアニメーションする(Inboxと同じ動き)、というサンプルを動かそうと思います。

まずはCoordinatorLayoutクラスを、以下のように普通にViewGroupとして使ってみました。 実行してみると、SnackbarとFABが重なってしまいました。意図した動きになりません。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator_layout"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">

        <Button
            android:id="@+id/show_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Show Snackbar"
            />

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:src="@drawable/ic_add_white_24dp"
            design:fabSize="normal"
            />

    </RelativeLayout>
    
</android.support.design.widget.CoordinatorLayout>
findViewById(R.id.show_button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        final CoordinatorLayout layout = (CoordinatorLayout) findViewById(R.id.coordinator_layout);
        String message = "Snackbar test.";
        Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
                .setAction("UNDO", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // something
                    }
                })
                .show();
    }
});

f:id:androhi:20150603021308p:plain

FABにアンカーを設定する

片方のViewの動きに合わせて、もう片方のViewもアニメーションさせるには、anchorを設定しなければいけないとのことです。アンカーに指定出来るのは、CoordinatorLayoutの派生クラスになります。アンカーが設定されてるViewは指定出来ません。

さっきの失敗したxmlを、以下のように直してみました。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator_layout"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/show_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Show Snackbar"
            />

    </RelativeLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_add_white_24dp"
        design:fabSize="normal"
        design:layout_anchor="@id/coordinator_layout"
        design:layout_anchorGravity="bottom|right"
        />

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

やったことは、FABをCoordinatorLayout直下に移動して、anchorを設定しました。 layout_anchorに対象のViewIDを指定し、layout_anchorGravityで特定の位置に寄せています。

これを実行すると、きちんとSnackbarとFABが重ならない動きになりました。

f:id:androhi:20150603021328p:plain

振る舞いの実装について

ところでこの場合、実際にFABがアニメーションする仕組みを提供しているのは、FloatingActionButton.Behaviorクラスです。

これはCoordinatorLayout.Behaviorを拡張したクラスで、他にもAppBarLayout.Behavior/AppBarLayout.ScrollingViewBehavior/SwipeDismissBehaviorなどが用意されていて、それぞれどんなアニメーションをするかなどの振る舞いを実装しているようです。

CoordinatorLayoutの子Viewになるクラスには、DefaultBehaviorアノテーションを使うことで、カスタムしたBehaviorも設定可能なようです。試してませんが、以下のように使えば良さそうです。

@DefaultBehavior(CustomView.Behavior.class)
public class CustomView extends View {
    // something
    ...
    public static class Behavior extends android.support.design.widget.CoordinatorLayout.Behavior<CustomeView> {
        ...
    
        public boolean layoutDependsOn(CoordinatorLayout parent, CustomView child, View dependency) {
            ...
        }
    
        public boolean onDependentViewChanged(CoordinatorLayout parent, CustomeView child, View dependency) {
        ...
        }
    }
}

まとめ

  1. SnackbarはToastにアクションを追加したようなUIなので、うまく使えばアプリ内通知がスッキリしそうだと感じます
  2. CoordinatorLayoutは一見地味だけど、ユーザー操作に合わせてUIをアニメーションさせるのに強力な仕組みだと思います
  3. CoordinatorLayout.Behaviorは、あまり凝ったことしなければFABなどのデフォルトのBehaviorで十分だろうけど、カスタムも出来るので使い勝手が良さそう

Design Support Library v22.2.0について Part 1

先日リリースされたDesignSupportLibraryについて調べたことを書いていきます。 なおこの記事で動作確認しているのは、SupportLibrary v22.2.0を使っています。

動作確認したサンプルプロジェクトでは、build.gradleに以下を追記しました。

dependencies {
    compile 'com.android.support:appcompat-v7:22.2.0'
    compile 'com.android.support:design:22.2.0'
}

TextInputLayout

ドキュメントを読むと、このクラスはLinearLayoutのサブクラスとなっていて、EditTextのヒントテキストをフローティングラベルとして見せることが出来るようになるとのことです。あとはエラー表示にも対応しているみたいですね。

FloatingLabel付きEditTextの表示

以下のコードを動かしてみました。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity">

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

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="hint text"/>

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

</LinearLayout>
public class MainActivity extends AppCompatActivity {

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

        TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout);
        textInputLayout.setErrorEnabled(true);
        textInputLayout.setError("Error!");
    }

f:id:androhi:20150602013825p:plain f:id:androhi:20150602013843p:plain

Note: エラー表示を消したいときは、setErrorEnabled(false)にすればいいようです。

ChildViewの追加

TextInputLayoutクラスにはaddView()メソッドが用意されているので、動的にViewを追加出来ます。 ただし、TextInputLayoutとEditTextは1対1でないといけないようです。試しにさっきのコードを以下のようにすると、例外が発生しました。

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

        TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout);
        textInputLayout.setErrorEnabled(true);
        textInputLayout.setError("Error!");

        EditText editText = new EditText(this);
        textInputLayout.addView(editText, 0, 
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }
Caused by: java.lang.IllegalArgumentException: We already have an EditText, can only have one

JavaからEditTextを操作する場合は、以下のように書くとうまくいきました。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity">

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

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

</LinearLayout>
public class MainActivity extends AppCompatActivity {

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

        TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout);
        textInputLayout.setHint("hint text from setHint method");
        textInputLayout.setError("Error!");
        textInputLayout.setErrorEnabled(false);

        EditText editText = new EditText(this);
        textInputLayout.addView(editText, 0,
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));

        EditText editText2 = textInputLayout.getEditText();
        editText2.setText("text from setText method");

セットしたEditTextはgetEditTextで取り出すことが出来ます。 注意しなければいけないのは、テキストはEditTextにセットしますが、エラーとヒントはTextInputLayoutにセットする必要があります。

f:id:androhi:20150602015625p:plain

FloatingActionButton

待望のFloatingActionButtonクラスですね。 MaterialDesignのUIコンポーネントの中で最もメジャーであるにも関わらず、長い間3rd Party Libraryや独自実装に頼らざるを得なかったので、個人的にはDesignSupportLibraryで一番うれしいです。

FloatingActionButtonについてはClass Overviewでも、特定のアクションを促すためのボタンだと定義しています。 パッケージツリーを見ると、ImageViewを継承しているようです。 デフォルトの背景色は、アプリのテーマのcolorAccentが割り当てられます。

ちなみにこのクラスは、FloatingActionButton.Behaviorクラスを持っていますが、主にSnackBarと連携する際に用いるので、次回書くことにします。

FABの表示

表示するだけであれば、layoutファイルに以下のように書くだけで大丈夫でした。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/mini_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_add_white_24dp"
        design:fabSize="mini"
        />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/normal_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_add_white_24dp"
        design:fabSize="normal"
        />

</LinearLayout>

f:id:androhi:20150602014006p:plain

ちゃんと影も付いてます。タップ時には影も濃くなって、浮き上がるアニメーションになります。

FloatingActionButtonクラスは専用のAttributesを持っていて、FABのサイズを2パターン指定出来るようです。

背景色の変更

背景色を変えるには、setBackgroundTintList()メソッドが推奨されています。引数にはColorStateListを渡すので、ボタンの状態ごとに色を指定出来ます。

FloatingActionButton miniFab = (FloatingActionButton) findViewById(R.id.mini_fab);
FloatingActionButton normalFab = (FloatingActionButton) findViewById(R.id.normal_fab);
miniFab.setBackgroundTintList(ColorStateList.valueOf(Color.YELLOW));
normalFab.setBackgroundTintList(ColorStateList.valueOf(Color.GREEN));

f:id:androhi:20150602014027p:plain

あとsetRippleColor()というメソッドも用意されていて、KitKat以降のデバイスなら波紋のような描画をするとあるので、以下のコードを手元のKitKat端末で試しましたが確認出来ませんでした。 使い方が間違っているのかもしれません。

FloatingActionButton normalFab = (FloatingActionButton) findViewById(R.id.normal_fab);
normalFab.setRippleColor(Color.CYAN);

まとめ

  1. TextInputLayoutは、FloatingLabelを実現するためにEditTextをラップするLayoutクラスでした
  2. FloatingActionButtonは、色をカスタムするための便利メソッド複数用意されていました

AndroidNDKを含むプロジェクトをCircleCIでビルドする

標準的なAndroidプロジェクトのcircle.yml

2015/5/12現在では、公式サイトによると主要なAndroidSDKは、VMにプリインストールされているとのことです。 そのため、以下のような非常にすっきりしたYAMLファイルで、Androidプロジェクトをビルドすることが出来るようになっています。(テストは省略しています。)

machine:
  java:
    version: openjdk7
test:
  override:
    - echo "Nothing to do here"
deployment:
  master:
    branch: master
    commands:
      - ./gradlew assemble

もしプリインストールされたSDKに、使用したいものが無い場合だけアップデート処理を追記してあげればいいみたいです。

dependencies:
  pre:
    - echo y | android update sdk --no-ui --all --filter "package-name"

AndroidNDKを使用する場合のcircle.yml

公式によると、

We also preinstall the Android NDK; it can be found at $ANDROID_NDK.

とあるので、AndroidNDKもプリインストールされてるようです。 ところが、そのままAndroidNDKを使ったプロジェクトをビルドさせると、以下のようなエラーが出てNDKのタスクで失敗してしまいました。

> Building 2%NDK not configured.
> Building 2%  Download the NDK from http://developer.android.com/tools/sdk/ndk/.Then add ndk.dir=path/to/ndk in local.properties.
> Building 2%  (On Windows, make sure you escape backslashes, e.g. C:\\ndk rather than C:\ndk)

失敗する原因は、$ANDROID_NDKではAndroidNDKのホームディレクトリとして認識出来ないことでした。 そのため以下のように、$ANDROID_NDK_HOMEにプリインストールされているAndroidNDKを指定するとうまくいきました。($ANDROID_NDK_HOMEに言及した元ソースは、思い出せませんでした…)

machine:
  java:
    version: openjdk7
  environment:
    ANDROID_NDK_HOME: /usr/local/android-ndk
test:
  override:
    - echo "Nothing to do here"
deployment:
  master:
    branch: master
    commands:
      - ./gradlew assemble

ビルド中の突然死を予防するcircle.yml

特に具体的なエラーを吐くことなく、ビルドが失敗するケースがありました。 主な対策としては、下記2点が有効なようです。

  1. JVMのHeapSizeを制限する
  2. PreDexingを無効化する
machine:
  java:
    version: openjdk7
  environment:
    ANDROID_NDK_HOME: /usr/local/android-ndk
    JAVA_OPTS: "-Xmx2048m -XX:MaxPermSize=1024m"
test:
  override:
    - echo "Nothing to do here"
deployment:
  master:
    branch: master
    commands:
      - ./gradlew assembleDogfood -PdisablePreDex

JVMのHeapSizeを制限する

JAVA_OPTS: "-Xmx2048m -XX:MaxPermSize=1024m"

ある程度の規模のプロジェクトになると、仮想マシンのメモリを食い尽くしてしまうことが多々あるので、Gradleが使えるリソースに制限をかけてあげると安定します。

PreDexingを無効化する

これは公式サイトのDisable Pre-Dexing to Improve Build Performanceの部分に、詳細が書かれています。 その部分を一部引用すると、以下のように書かれています。

Because CircleCI always runs clean builds this pre-dexing has no benefit; in fact it makes compilation slower and can also use large quantities of memory. We recommend disabling pre-dexing for Android builds on CircleCI.

CircleCIでビルドするときは、PreDexingを無効化してねということらしいです。 やり方は、GradleAndroidPluginのサイトに載ってます。(上記CircleCIのサイトからもリンクが貼られてました。)

まずプロジェクトの直下にあるbuild.gradleに、以下のGradleタスクを追加します。

project.ext.preDexLibs = !project.hasProperty('disablePreDex')

subprojects {
  project.plugins.whenPluginAdded { plugin ->
    if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
      project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
    } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
      project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
    }
  }
}

次にcircle.ymlでビルドコマンドを叩く箇所に、dsablePreDexプロパティを付与してあげます。 これでpreDexタスクは実行されなくなります。

./gradlew clean assemble -PdisablePreDex

参考にさせて頂いたサイト

まとめ

AndroidNDKを使う場合は、$ANDROID_NDK_HOMEの設定が必要でした。 JVMのHeapSize制限は必要に応じて使えばいいけど、PreDexingの無効化はやっておいたほうがよさそうです。

またcircle.ymlの書き方は、まだまだ変わっていきそうなので、定期的に公式サイトをチェックした方がよさそうだなと思いました。 この記事では触れませんでしたが、エミュレーターの起動やテストまわりも書きやすくなってました。

自分史上最高に英語学習に取り組んでみようとしてる

今年に入ってから英語でコニュニケーション出来ないために、「ぐぬぬ…」となる出来事が立て続けに起こりました。仕事で1回、コミュニティ活動で1回、あと勉強会でも1回ありました。

分からない単語は調べながら、そこそこ英語のドキュメントが読めれば十分だと思ってた時期が僕にもありました。

きっと多くの人が同じような感じだとも思ってました。 ところがそうでは無いことも、今年になって急激に知ることとなってしまったんです。

知り合いのエンジニアの多くは海外のエンジニアと英語でコニュニケーションを取ってるし、海外のmeetupでLTもするし、同僚のディレクターは僕の隣で某外資系企業の人と英語でコニュニケーション取ってるんです。

これでもか!ってくらい、英語が話せないことに劣等感を感じまくりました。もうこれは、本気で英語に取り組むラストチャンスかな、という気持ちになりました。

しばらく英語学習系のアプリを試したり、PCサイトやPodcastなどなど手を出してみたのですが、いまいちこのまま進んでいいのか自信が持てない日が続いてました。

そんな時、先日「英語上達完全マップ」にたどり着きました。コンテンツを読んでいたら、ずっともやもやしてたものがスッキリした気持ちになり、これだと思いました。

具体的には、「音読」と「瞬間英作文」を柱とした基礎力の強化を重視している学習法を詳細に解説してくれている点に、これは実践してみたいと感じることが出来ました。

いろいろと情報収集して、このへんを参考にしつつ第一弾として3冊の参考書を買いました。

買ったのは以下の3冊です。基礎の基礎から勉強し直そうと思います。 秋頃には一度TOEICを受けてみる予定です。

くもんの中学英文法―中学1?3年 基礎から受験まで (スーパーステップ)

くもんの中学英文法―中学1?3年 基礎から受験まで (スーパーステップ)

みるみる英語力がアップする音読パッケージトレーニング(CD BOOK)

みるみる英語力がアップする音読パッケージトレーニング(CD BOOK)

どんどん話すための瞬間英作文トレーニング (CD BOOK)

どんどん話すための瞬間英作文トレーニング (CD BOOK)