will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

ImeFragmentというライブラリを公開しました!キーボード開発でもFragmentを使う!

この記事はCyberAgent Developers Advent Calendar 201620日目の記事です。

www.adventar.org

19日目はstrskさんでGKEのノードプールを利用したKubernetesのアップグレードでした。 ちなみにstrskさんは元々飲食業界ではたらいていてCSで入社→今はAbemaTVでGKE運用してる方です。スゴイ、、、!

明日は...○○です。

アドベントカレンダーには去年から参加し始めていて、2015年に書いた記事はこちらです。

同期系スマホアプリのリリースサイクル・テストについて - will and way

1年前はAppleのアプリレビューが1週間くらいだったのか。。。2, 3日で返ってくるようになったのは革命的な出来事だったな〜。

さて、本題のImeFragmentに入っていきましょう!

ImeFragmentというライブラリを公開しました

https://github.com/matsuokah/ImeFragmentgithub.com

一言で言うと、InputMethodServiceでもFragmentとほぼ同じように使って実装ができるというライブラリです。
とりあえず作った感じなので、整理はこれからですが。

IME開発のキモとなりそうなポイントをAndroidのAdventCalendar::day11でInputMethodService(キーボード)開発の勘所となりそうな項目という記事に書きました。

その中にServiceではFragmentは使えないという項目がありました。ImeFragmentはそれを解決しライブラリ化したものです。

Fragmentが使えると何が良いのか?

アプリを実装している感覚で部品が開発できる。アプリの実装が使いまわしやすいということです。

アプリ開発の中でFragmentというインターフェースに慣れ親しんでいます。それは、フラグメントはアプリのライフサイクルだったり、ViewPagerのように動的にアタッチ/デタッチがされた場合のハンドリングだったりします。

Fragmentのようなインターフェースを持つクラスがないので、InputMethodServiceの実装がもりもりになってしまいます いわゆる、"マッチョなActivity"のように、"マッチョなInputMethodService"が避けられない状態です。

"マッチョなInputMethodService"を分割していくということは、コントローラとなりうるクラスを作るということになります。また、そのコントローラの要件はInputMethodServiceに応じたライフサイクルを持つことや、アプリ同様にonTrimMemoryのようなアプリのライフサイクルにも対応している必要があります。

結局、Fragmentが欲しいということです。

Fragmentという粒度のクラスができることによって、Fragmentが依存したいクラス(PresenterやUseCaseなど)の単位もアプリと同じように使えますし、使うイメージも湧きやすいです。

Imeに対するImeFragmentのライフサイクルのマッピングについて

InputMethodServiceはActivityよりも幾つかのライフサイクルのステップが少ないことや、アクションバーを持たないなどの違いがありますが、基本となるonCreateからonDestroyまでのライフサイクルは同じようにマッピングすることが出来ました。
なので、実際にはActivityで使う場合と同じように使うことが出来ます。

サンプルの紹介

InputMethodServiceでViewPagerを使ってみた例です。

f:id:matsuokah:20161220002736g:plain

build.gradle
dependencies {
    compile 'jp.matsuokah.imefragment:imefragment:1.0.1'
}

bintray, jcenterにホストしてあります。

SampleImeService.java
public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    adapter = new SampleFragmentPagerAdapter(getImeFragmentManager());

    WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics dm = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(dm);
    int windowHeight = dm.heightPixels;


    View wrapper = findViewById(R.id.ime_wrapper);
    ViewGroup.LayoutParams params = wrapper.getLayoutParams();
    params.height = (windowHeight * INPUT_VIEW_HEIGHT_PERCENTAGE) / 100;
    wrapper.setLayoutParams(params);

    ViewPager pager = (ViewPager) findViewById(R.id.pager);
    pager.setAdapter(adapter);

    return super.onCreateInputView();
  }
}
SamplePageFragment.java
ublic class SamplePageFragment extends ImeFragment {

  private static final String POSITION_KEY = "position_key";

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

    View view = inflater.inflate(R.layout.fragment_ime_page, null);

    Bundle bundle = getArguments();
    int position = bundle.getInt(POSITION_KEY);

    TextView label = (TextView)view.findViewById(R.id.position);
    label.setText(String.valueOf(position));

    return view;
  }

  public static ImeFragment newInstance(int position) {
    ImeFragment fragment = new SamplePageFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(POSITION_KEY, position);
    fragment.setArguments(bundle);
    return fragment;
  }
}

解説

public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    return super.onCreateInputView();
  }
}

onCreateはこれはお作法になります。setContentでrootとなるViewをImeFragmentServiceでinflateしています。

理由は、以下の2つです

  1. InputMethodServiceはonCreateInputViewの時にViewをさわる状態ができている
  2. Fragmentを管理する機構の中で親のビューを必要とするため

また、ImeFragmentServiceにはfindViewByIdを実装したので、ActivityのようにViewを取得できるようになっています。

Fragmentに関してはほぼ、解説する必要はないですね。元のFragmentと全く同じです。

実装内容について

本家のサポートライブラリの実装を参考にし、必要なメソッドを再実装していった形になります。Fragmentの管理に使われているクラスにはActivity/Serviceに依存しない実装のものがいくつかあったのですがパッケージプライベートな処理に手を入れる必要があり、コピーしてパッケージにいれています。サポートライブラリのクラスをそのまま使っていたりもします。インターフェースとか。

変わっている点は以下のとおりです。

  • Activityにあって、Serviceにない機能の削除
    • Picture in PictureやMultiWindowMode、OptionsMenuはImeでは不要。
  • InputMethodServiceに合わせて、fragmentのライフサイクルのマッピングを調整
    • onCreate ⇔ onCreate
    • onCreateInputView ⇔ onCreateView
    • onStartInput ⇔ onStart
    • onWindowShown ⇔ onResume
    • onFinishInput ⇔ onPause
    • onWindowHidden ⇔ onStop
    • onDestory ⇔ onDestroy

バグや機能追加のPRまってます!

Androidの開発を始めてから2ヶ月の人間が作ったライブラリなので、保証はできません!リファクタもまだまだ。。。 ということで、コントリビューションをお待ちしております!issueだけでもっ!

https://github.com/matsuokah/ImeFragmentgithub.com

まとめ

InputMethodServiceでもFragmentを使えるようにしました。これによってActivityを作る感覚でキーボードを開発できるようになりました!
趣味でキーボード触ってるんですが、アプリとまた違った可能性を感じています!
変換予測とか考えるのタノシイっ(๑•̀ㅂ•́)و✧

また、このライブラリを開発した副産物として、ActivityとFragmentの関係やFragmentのライフサイクルがどのようなコードなのかを知ることが出来ました。
AndroidのBaseやAndroid Support Libraryのリポジトリ、読んでみると面白いですね!