EDIT MODE

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

ToolWindow タイプの Intellij Plugin 作成の Tips

初めて Intellij Plugin を作ってみました。

どんな Plugin を作ったかは、前の記事に書いたので興味あるかたは読んでみてください。 また作成した Plugin のコードGitHub で公開しています。

Intellij Plugin を作ってみて感じこと

ちょっとググると分かりますが、言語を限定しなくても関連する情報が少ないです。

後述するブログやサイトと、その中で紹介されてるブログやサイトでほぼめぼしい情報が底をつきます。

あとはもう、OSS として公開されてる Intellij Plugin のコードを参考にするか、Intellij IDEA CE のコードを参考にするしかありませんでした。

とはいえ、メインの技術は Java Swing という枯れた技術なので、そこは情報量豊富でした。 そのため調べながら作るとしたら、以下のような組み合わせを繰り返す感じになると思います。

  • Plugin 作成の流れや基本 >> 公式サイトや Plugin 作成について書かれたブログを見る
  • UI まわりの基本 >> Swing 関連のサイトやブログを見る
  • ピンポイントでの実装方法 >> OSS の Plugin のコードを読む
  • 上記でも解決できない問題 >> Intellij IDEA CE のコードを読む

Plugin のタイプ

AndroidStudio や Intellij IDEA などを実際に使ってる人には説明不要かと思いますが、Plugin をざっくり3タイプに分けるとこんな感じだと思ってます。

  1. ほぼ UI を持たないサポート系
  2. 特定のタイミングで機能するダイアログ系
  3. 常駐?して機能するツール

1 は CodeGenerator とか Editor 系の Plugin を指してます。

2 はそのまんまですが Importer 系とか FileGenerator とかいろいろありますね。

3 は ProjectTree とか BuildTool 系で、いわゆる画面の左端や右端、もしくは下部に表示/非表示を切り替えられるタイプの Plugin です。

ToolWindow の作成

Plugin プロジェクトの作成方法などは、後述するサイトなどで詳しく紹介されているのでここでは触れません。 まずは ToolWindow を表示するために必要な手順を書いていこうと思います。

1. ToolWindow にコンテンツをセットするクラスを作る

以下のように ToolWindow に表示する UI をセットするためのクラスを追加します。ポイントは、ToolWindowFactory インターフェースを実装する点です。

public class AndroidDrawableViewerToolWindowFactory implements ToolWindowFactory {

    @Override
    public void createToolWindowContent(Project project, ToolWindow toolWindow) {
        JLabel label = new JLabel("Hello World");

        final ContentManager contentManager = toolWindow.getContentManager();
        final Content content = contentManager.getFactory().createContent(label, null, false);
        contentManager.addContent(content);
    }
}

createContent(JComponent, String, boolean) メソッドで、第一引数に Swing の UIConponent をセットでき、主にそれが ToolWindow 内に表示されます。

2. plugin.xml に ToolWindow を生成する宣言を記述する

作成する Plugin の初期化時に、1 で作成した処理が動くように宣言する必要があります。 記述するのは、 <extension> タグの中です。

  <extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <toolWindow anchor="right" canCloseContents="true" id="DrawableViewer"
                factoryClass="com.androhi.androiddrawableviewer.AndroidDrawableViewerToolWindowFactory" />
  </extensions>

各属性について詳しくは公式サイトの ここ に書かれていますが、補足すると factoryClass の指定は完全修飾名(パッケージ名込み)で書かないとダメでした。

3. ToolWindow を表示する

1 と 2 を作成して実行すると、1 で createContent(JComponent, String, boolean) メソッドに渡した JComponent(ここだと"Hello World"と表示するラベル)が表示される ToolWindow を表示できると思います。

ToolWindow の表示切り替えは、メニューから行う方法と ToolButtons で行う方法があります。

メニューでの切り替え

View > ToolWindowsにPlugin名

f:id:androhi:20150723231446p:plain

ToolButtons での切り替え

View > Tool Buttons がオンの状態で plugin.xml で指定した anchor の場所のボタン(ここではWindow右端)

f:id:androhi:20150723231510p:plain

ToolWindow に ToolBar を設置する

下図のように ToolWindow 内には、何かしらのボタン郡があるものが多いと思います。この領域は ToolBar と呼ばれるものらしく、コンテンツ部分とは独立して配置されるようです。

f:id:androhi:20150723233841p:plain

このパターンの ToolWindow には、SimpleToolWindowPanel という JPanel のサブクラスを使うのが一般的らしいです。(主に Intellij CE のコードを見て知りました。)

public class DrawableViewer extends SimpleToolWindowPanel {

    public DrawableViewer(final Project project) {
        super(true, true);
        this.project = project;

        setToolbar(createToolbarPanel());
        setContent(createContentPanel());
    }

    private JComponent createToolbarPanel() {
        final DefaultActionGroup actionGroup = new DefaultActionGroup();
        actionGroup.add(new AnAction());
        final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("AndroidDrawableViewer", actionGroup, true);
        return actionToolbar.getComponent();
    }

    private JScrollPane createContentPanel() {
        String projectPath = project.getBasePath();
        ...
    }
    ....
}

上のコードで、setToolBar(JComponent) メソッドで ToolBar 領域の UI を、setContent(JComponent) メソッドで Content 領域の UI をそれぞれセットする仕組みになっています。

setContent() の方は、割といろいろな JComponent がセットできますが、setToolBar() の方は AnAction クラスもしくはサブクラス(要はアイコンボタン)のグループを内包した ActionToolbar の Component をセットするのがセオリーなようです。

ここでは見やすいように new Action() をセットしていますが、実際には固有のアイコンや処理を定義するので、AnAction のサブクラスのインスタンスをセットします。そのあたりは、GitHubのコード で全体を見ていただいた方が分かり易いかもしれません。

ToolWindow の表示更新

これがかなり悩みました。

以下に書いた内容も正しいか自信がありません。もっとよい方法をご存知の方は、ぜひ教えてください。

やりたかったことは、Action があったときに ToolWindow のコンテンツを新しいデータを反映した後、再描画するという内容です。

デコンパイルした Intellij 側のコード見たりいろいろと試したのですが再描画できず、結局コンテンツを再生成してセットし直すことで、期待した動きになりました。

以下は先程の ToolBar のボタンをクリックしたときのアクションクラスです。

public class EditTargetResDirAction extends AnAction {

    public EditTargetResDirAction() {
        super("Edit directory", "Edit target resource directory", AllIcons.General.Settings);
    }

    @Override
    public void actionPerformed(AnActionEvent anActionEvent) {
        Project project = anActionEvent.getProject();
        ...
        final VirtualFile file = FileChooser.chooseFile(descriptor, project, selectDir);
        if (file != null) {
            ...
            resetContent(project);
        }
    }

    private void resetContent(Project project) {
        DrawableViewer drawableViewer = new DrawableViewer(project);
        ContentManager contentManager = ToolWindowManager.getInstance(project)
                .getToolWindow(DrawableViewer.TOOL_WINDOW_ID).getContentManager();
        Content content = contentManager.getFactory().createContent(drawableViewer, null, false);

        contentManager.removeAllContents(true);
        contentManager.addContent(content);
    }

getToolWindow(String) メソッドに plugin.xml に記述した ToolWindow の ID を渡すと同じ ToolWindow が取得できるので、それに対して再生成した JComponent インスタンス(=drawableViewer)をセットし直しています。 セットするときは、Add するのでその前に古い方は Remove しています。

まとめ

ToolWindow まわりは Swing では無く Intellij の仕組みなので、特に公開されてる情報が少なく途中で心が折れかけました。

でも ToolWindow で実現する Plugin は、そのコンテンツを見ながらコードを書けたりするので、いろいろと楽しい Plugin が作れるんじゃないかなーと感じました。

加えて Intellij IDEA 14 から実装されたデコンパイル機能が、ものすごく重宝しました。これのおかげで公開まで辿りつけた気がします。

そしてめでたく JetBrains Plugin Repository に公開されたので、もし興味ある方は感想などいただけると嬉しいです。

AndroidDrawableViewer f:id:androhi:20150723230925p:plain

参考にしたサイト

Android Drawable Viewer という Intellij Plugin を作ってます

Pluginを作り始めた経緯など

Androidアプリ開発をしていて、今のプロジェクトのdrawable配下の画像を一覧で確認したいなと思い、そういったIntellij IDEA Pluginを探したのですが見つからなかったので自作することにしました。

プロジェクトも比較的大きなものだと、いろいろな画像ファイルをアプリにバンドルします。特にアイコン関連が多くなりがちです。 さらにAndroidでは様々なディスプレイ解像度に対応するため、同じ画像を2パターン、3パターン、あるいはそれ以上必要になってくるかと思います。

そのあたりの管理うんぬんの前に、画像のプレビュー/ファイル名/対応する解像度など、全体を把握したいことが多々ありました。今作っているのは、それらを解決するためのPluginです。

どんなPluginか?

Androidプロジェクトのdrawableフォルダに入っている画像ファイルの情報を、リスト化するPluginです。まだbeta版ですが、ソースGitHubに上げています。

実行サンプルは、こんな感じです。

f:id:androhi:20150716024808p:plain

今後追加したい機能

まだまだ機能やUIは改善すべきとこが多く、継続してアップデートしたいと思っています。 直近で検討している改善点は、以下のようなものです。

  • リソースディレクトリの設定
  • 画像の詳細情報(サイズや色)の表示
  • 任意のdrawableフォルダの絞り込み表示

このあたりが実装できたら、Jetbrains Plugin Repositoryにも公開したいなと考えています。

Plugin作成で参考にしたサイト

Intellij IDEAを触ったことがあれば、実装自体はそれほど難しくない気がします。個人的にはSwingのUI Componentsを初めて触ったので、その使い方に四苦八苦してます。

以下、メインで参考にさせて頂いたサイトなどです。執筆者や開発者の方々、ありがとうございます。

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は、色をカスタムするための便利メソッド複数用意されていました