phicdy devlog

Androidアプリ開発やその他技術系の記事をたまに書きます

AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

【Android】MaterialShowcaseViewライブラリでチュートリアルを実装する

MyCurationの1.2.1でチュートリアルを追加しました。というのもインストールされてもすぐアンインストールされているようで、説明が足りなかったかな・・・と思い始めたためです。 実装を自分でやってもよかったのですが、デザイン的なセンスがないのと、ある程度テンプレート化されたものがあるのではということでライブラリを探しました。 今回はMaterialShowcaseViewというライブラリを使ってチュートリアルを実装してみました。

f:id:phicdy:20170625104753g:plain

設定

GitHubのページ通りにbuild.gradleを編集

プロジェクトのbuild.gradle

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

アプリのbuild.gradle

dependencies {
  compile 'com.github.deano2390:MaterialShowcaseView:1.1.0@aar'
}

使い方

MaterialShowcaseViewでは特定のViewにハイライトを当ててチュートリアルを行います。 実装できるパターンとしては、(1)単純に追加ボタンなど1つだけを説明するパターンと、(2)ボタン1を押して次にボタン2を押して・・・といったように連続して説明をするパターンがあります。

MyCurationでは最初のRSS購読のチュートリアルを入れました。 RSS購読の流れとしては

  1. メイン画面からToolbarの「+」ボタンを押し、検索画面に行く(メイン画面)
  2. RSSを購読するサイト検索する(検索画面)
  3. サイトを開いたら追加ボタンを押す(検索画面)

です。

まずメイン画面で実装した単純に追加ボタンなど1つだけを説明するパターンです。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Menu settings...

    // Start tutorial at first time
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            View view = findViewById(R.id.add);
            new MaterialShowcaseView.Builder(TopActivity.this)
                    .setTarget(view)
                    .setContentText(R.string.tutorial_go_to_search_rss_description)
                    .setDismissText(R.string.tutorial_next)
                    .singleUse(SHOWCASE_ID)
                    .setListener(new IShowcaseListener() {
                        @Override
                        public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {

                        }

                        @Override
                        public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
                            goToFeedSearch();
                        }
                    })
                    .show();
        }
    });
    return true;
}

Builderパターンを使ってMaterialShowcaseView.Builderを作成していきます。今回はToolbar上の「+」ボタン(R.id.add)にハイライトを当てたかったのでonCreateOptionsMenu()の中でチュートリアルを作成します。 まずsetTarget()でハイライトするViewを指定します。次にチュートリアルの説明文と、チュートリアルを消す部分のテキストを設定します。singleUse()でIDを指定することで初回のみチュートリアルを表示し、同じIDのチュートリアルは今後表示しないように設定できます。今回はチュートリアルが閉じたときに次の画面(検索画面)に移りたかったので、setListener()でIShowcaseListenerをセットし、チュートリアルが閉じられたときに呼ばれるonShowcaseDismissed()で次の画面に飛んでいます。設定が終わったら最後にshow()でチュートリアルを表示します。

使ったメソッドのまとめです。

メソッド 説明
setTarget(View view) ハイライトするViewを指定
setContentText(int resource) 説明文を指定
setDismissText(int resource) タップして閉じる部分の文を指定
singleUse(String showCaseId) 1度だけの表示を管理するためのID。これを設定するとアンインストールしない限りは同じIDのチュートリアルは表示されない
setListener(IShowcaseListener listener) リスナの設定。表示時、消えた時のハンドリング
show() 表示する

次に検索画面で実装した連続して説明をするパターンです。

@Override
public boolean onCreateOptionsMenu(Menu menu) { 
    // Menu settings...

    // Start tutorial at first time
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            View view = findViewById(R.id.search_rss);
            ShowcaseConfig config = new ShowcaseConfig();
            config.setDelay(500); // half second between each showcase view

            MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(FeedSearchActivity.this, SHOWCASE_ID);
            sequence.setConfig(config);

            // Search tutorial
            sequence.addSequenceItem(
                    new MaterialShowcaseView.Builder(FeedSearchActivity.this)
                            .setTarget(view)
                            .setContentText(R.string.tutorial_search_rss_description)
                            .setDismissText(R.string.tutorial_next)
                            .build()
            );

            // Add button tutorial
            sequence.addSequenceItem(
                    new MaterialShowcaseView.Builder(FeedSearchActivity.this)
                            .setTarget(fab)
                            .setContentText(R.string.tutorial_add_rss_description)
                            .setDismissText(R.string.tutorial_close)
                            .setDismissOnTouch(true)
                            .build()
            );

            // Open software keyboard if tutorial already finished
            if (sequence.hasFired()) {
                searchView.setIconified(false);
            }

            sequence.start();
        }
    });
    return true;
}

今度はMaterialShowcaseSequenceというクラスのインスタンスを作り、MaterialShowcaseViewのインスタンスを追加していってチュートリアルの流れを作ります。

さきほどと同様にBuilderパターンでMaterialShowcaseView.Builderを作っていき、今回は最後にbuild()を呼ぶことでMaterialShowcaseViewを作り、MaterialShowcaseSequenceに追加します。これを繰り返すことでチュートリアルを表示→閉じる→チュートリアルを表示→閉じる→・・・と流れを作れます。作り終わったらstart()でチュートリアルを始めます。

注意した点

Toolbar上のViewがNullになる

onCreateOptionsMenu()の中でViewをfindViewById()で取るとNullが返ってきて落ちるという現象がありました。Handler#post()を使うことで遅延を発生させてこれを回避しました。

参考

How To Get Action View Of Menu Item?

チュートリアル中にキーボードが開かないようにする

検索画面では開いたときにすぐキーボードが開くようにsearchView.setIconified(false)を呼んでいたのですが、チュートリアル中は邪魔です。MaterialShowcaseSequence#hasFired()でチュートリアルが既に終わったかを判定できるので、チュートリアルでないときのみキーボードを開くようにしました。

// Open software keyboard if tutorial already finished
if (sequence.hasFired()) {
    searchView.setIconified(false);
}

終わりに

MaterialShowcaseViewを使うことでかなり簡単にチュートリアルを作成しました。さくっと作れるので、凝ったチュートリアルが必要でなければこれで十分かなと思います。以上です。

2週間毎日GitHubにコードを上げ続けて思ったこと

最近私にしては珍しく2週間毎日GitHubにコードを上げ続けています。 (2週間ごときで何を言ってるだと思う方もいらっしゃるとは思いますが・・・)

f:id:phicdy:20160424132021p:plain


なんとなくで始めたことですが、自分にとっては結構よかったです。

よかった点としては

  • 定期的なアウトプットが行える
  • 毎日何かしらのコードを上げる必要があるので、機能の粒度を下げることを考える
  • 機能の粒度を下げるにはどうすればいいかという設計を考えるようになる
  • 今日は何を作ろうかと考えるようになる
  • 毎日勉強が行える
  • 隙間時間を見つけてコードを書くようになる
  • 草が生えるのがなんか嬉しい

などです。

毎日コードを上げるといっても、毎日何十コミットも上げる訳ではなく、疲れた日や時間のない日は.gitignoreを更新するだけにしたり、テストを書いて終わりにしたりと、自分の生活には影響ない範囲でできているのかなと思います。


一方でアウトプットを増やすとインプットが減るという話もありますので、コードを書きつつインプットの時間も取れるようにしていければいいのかなと思います。 (もちろん自分の自由な時間を削ることなく・・・)

ksss9.hatenablog.com

【Android】App Standbyがわからない

App StatndbyはAndroid 6.0になって追加された省電力の機能である。

Doze と App Standby 用に最適化する | Android Developers

アプリが以下の場合でないとき、そのアプリは"Idle"状態になり、"Idle"状態が長時間続くとバックグラウンドでの通信を1日1回しか行うことができなくなる。

  • ユーザがアプリを明示的に起動する
  • アプリのプロセスがフォアグラウンドにある (アクティビティまたはサービスがフォアグラウンドにある、または他のアクティビティやフォアグラウンドサービスからアプリが使わている時)
  • アプリが生成した通知をユーザがロック画面または通知領域で見る

また、端末が充電中の時は"Idle"状態から解放される。

以上である。


全然わからない。


例えば以下のようなことである。


  • “Idle"状態にはどの程度の時間でなるのか
  • 常駐Serivceが動いている場合は"Idle"状態に移らないのか
  • “Idle"状態が何時間続けばバックグラウンド通信を1日1回しかできなくなるのか
  • 1日1回とは1リクエストのことなのか
  • 1日でバックグラウンド通信規制→解放→バックグラウンド通信規制となった場合、2回目の規制は1回目から通信が規制されるのか


この辺りがわからないせいでどうテストを行えばいいのかがさっぱりわからない。 特に気になるのはバックグラウンド通信の規制に入る方法である。 一応GoogleにはApp Standbyをシュミレートする方法が載っているが、肝心のバックグラウンド通信規制にする方法は載っていない。


$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <packageName> true


テスト終了後に戻す。


$ adb shell am set-inactive <packageName> false
$ adb shell am get-inactive <packageName>  ← falseならOK


アプリが"Idle"状態かどうかはPowerManager#isDeviceIdleMode()で取得できるので、実機にて1時間ごとにAlarmでこの状態をログに出力し、2~3時間放置して様子を見てみたが、常にfalseだった(充電できないし、AlarmがDozeモードで規制されないようにしないといけないのでクソ面倒)。

PowerManager | Android Developers


その他海外含め色々調査してみたが、上記のApp Standbyの条件とGoogleが出しているシュミレート方法を載せている記事がほとんどで、App Standbyの詳細がわかる記事は見つからなかった(もっと探せばあるのかもしれない)。


この辺りをはっきりさせるのなら自分で全部調査するしかないのかな・・・ ただそこまでやる必要は無い気はする。


皆様はどう対応しているんでしょう。。。

【Android】 リモートリポジトリに繋がらない環境でJUnitを実行する

ネットワーク環境に問題がないときは、Googleのページのやり方で設定すればJUnitの環境設定は簡単だけど、Proxyなどでmaven centralに繋がらない場合の対処法


jarファイルのダウンロード

リモートリポジトリに繋がらないので、jarファイルをlibsフォルダに置いてコンパイルする

<dependencies>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>1.3</version>
    </dependency>
</dependencies>
  • ダウンロードしたjarファイルをlibsフォルダにコピー


build.gradle

compile fileTree(dir: 'libs', include: '*.jar', excludes: ['junit-4.12.jar', 'hamcrest-core-1.3.jar'])
testCompile files('libs/junit-4.12.jar')
testCompile files('libs/hamcrest-core-1.3.jar')


テストを書く

  • src/test/javaフォルダ以下にJUnit4のテストを書いていく
  • 実行は実行したいクラス、フォルダ、メソッドの上で右クリック -> Runで行う

【Android】自分でキュレーションを作るフィードリーダー「MyCuration」を公開しました

f:id:phicdy:20151011164400p:plain

MyCuration -キュレーションを作るフィードリーダー - Google Play の Android アプリ

だらだらと自分用に作ってたアプリがある程度まとまったので公開しました。

Android4.0以上対応です。

背景

開発の背景としては以下のような背景がありました。

  • feedlyが起動時に毎回ネットワーク通信をして嫌だった
  • RSSリーダーだと読みたくない記事も入ってきて毎回既読にするのが面倒だった
  • キュレーションアプリは不必要な情報が多いし、必要な情報が削られる

要はfeedlyもキュレーションアプリも使いたくないけど、既存のでいいのがなかったから作ったって感じです。

機能

  • RSSリーダー機能(記事の取得、未読管理など)
  • キュレーション機能(特定の単語が入っている記事をまとめる)
  • フィルター機能(特定の単語またはURLが入った記事を既読にする)

キュレーションの条件とフィルターの条件は今後増やしていきたいところ。考えてるのは、時間とかはてなブックマーク数、つぶやき数などなど

使っているライブラリ

Android-PullToRefreshはもうメンテナンスしていなくて、デフォルトでも提供していたと思うので、時間ができたらそちらに乗り換えたい。

開発環境

テストはJUnit単体テストで実機テストしかやってないけどできればUIAutomatorを使った自動UIテストとかも徐々にやっていきたい。

レポジトリ

github.com

終わりに

とりあえずリリースできたので、機能追加を徐々にやっていこうと思う

【Android】PreferenceActivity/PreferenceFragmentを使った設定画面

(追記) 2018/11/15 書き直しました。

phicdy.hatenablog.com

Androidの設定画面について

  • AndroidではAPI1から設定画面を生成してくれるPreferenceActivityが用意されている
  • Android 3.0で各設定画面をFragmentに分けるためのPreferenceFragmentが追加された
  • Android 4.0ではPreferenceActivityが拡張され、PreferenceFragmentと組み合わせることでハンドセットでは1画面、タブレットでは2画面の設定画面を自動的に作成できる

設定項目

クラス 説明
CheckBoxPreference オン/オフの設定項目
SwitchPreference オン/オフの設定項目
API14から
ListPreference リストの中から1つ選ぶ設定項目
MultiSelectListPreference リストの中から複数を選ぶ設定項目
EditTextPreference 入力からの設定

1画面の設定画面

  • 設定画面を2ページに分けない場合は、画面のルートに当たるandroid.R.id.contentに直接PreferenceFragmentを設定する

PreferenceActivity

public class SettingActivity extends PreferenceActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getFragmentManager().beginTransaction()
            .replace(android.R.id.content, new SettingFragment())
            .commit();
    }
}

PreferenceFragment

  • addPreferencesFromResource()で設定画面を定義したXMLを読み込む
  • 必要であれば、onActivityCreated()で各設定項目の初期値等を設定する
  • XMLで設定したPreferenceを取得するにはandroid:keyを使い、PreferenceFragment#findPreference()で行う
public class SettingFragment extends PreferenceFragment {

    public SettingFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.setting_fragment);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        initView();
    }

    private void initView() {
        ListPreference listPreference = (ListPreference)findPreference(getString(R.string.key_list_preference));
        listPreference.setValueIndex(0);
    }

}

PreferenceFragmentから読み込む設定XML

  • PreferenceFragmentから読み込むXMLのトップはPreferenceScreenでなければならない
  • 各Preferenceにはandroid:keyを設定しなければならない。これが各Preferenceを特定するIDとなる。
  • カテゴリを設定する場合はPreferenceCategoryを使う。android:keyは必須ではない。
  • ListPreferenceの場合、選択候補に表示されるリスト(android:entries)と実際の値のリスト(android:entryValues)を設定する必要がある
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    <PreferenceCategory android:title="Category Title" >
        <ListPreference
            android:title="Title"
            android:key="@string/key_list_preference"
            android:entries="@array/entry_sample"
            android:entryValues="@array/entry_sample_values"/>
    </PreferenceCategory>

</PreferenceScreen>

その他の属性は以下を参照

http://developer.android.com/reference/android/preference/Preference.html

array.xml

ListPreferenceで使う値を定義する

<resources>
    <array name="entry_sample">
        <item>@string/entry1</item>
        <item>@string/entry2</item>
    </array>

    <string-array name="entry_sample_values">
        <item>1</item>
        <item>0</item>
    </string-array>

</resources>

string.xml

  • android:keyに使う文字列はXMLとPreferenceFragmentで共有するので、string.xmlに定義しておく
<resources>
    <string name="entry1">Entry1</string>
    <string name="entry2">Entry2</string>
    <string name="key_list_preference">KeyListPreference</string>
</resources> 

初期値設定

XMLで初期値を設定する場合はandroid:defaultValueに設定する。SharedPreferenceから保存した値を読み込んでそれに応じて初期値を設定する場合など、Javaコードの中で初期値を設定を行う場合はPreferenceによって設定方法が異なる

SwitchPreference

setChecked()で行う

SwitchPreference switchPref = (SwitchPreference)findPreference(getString(R.string.key_internal_browser));
switchPref.setChecked(true);

ListPreference

  • ListPreferenceのentryValuesのstring-arrayの各itemが数字だとsetDefaultValue()が動かなかった
  • setValueIndex()を使うことで解決
  • entryValuesをinterger-arrayにするとNullPointerExceptionで落ちてしまう

参考

【Rasberry Pi】カメラモジュールでmmal: No data received from sensorエラー

Rasberry Pi B+とカメラモジュールを買ってみたので使ってみたところエラーで買い直しになった・・・


Raspberry Pi Camera Board | Raspberry Pi Video Module Raspberry Pi Camera Board | Raspberry Pi 【通販RS】

$ raspistill -o test.jpg
mmal: No data received from sensor. Check all connections, including the Sunny one on the camera board


調査

設定は以下コマンドで行った。

# 最新カーネルにアップデート
$ sudo apt-get update
$ sudo apt-get upgrade

# 8 Advanced Options -> A5 Enable Camera -> Enable
$ sudo raspi-config


再起動後、vcgencmdコマンドで接続チェックをしたところ、カメラの接続はできている模様・・・raspistillコマンドを打った際にもカメラのランプ自体は付いている。

$ vcgencmd get_camera
supported=1 detected=1


そもそも同じエラーでぐぐってみても87件しかそもそも検索結果がない。

"mmal no data received from sensor" - Google 検索


ソフトウェア側の問題でなんとか解決できないかと思って下記URLなどを参照したが改善される様子はなし・・・

そもそも解決している人が見つからなかった。


https://www.raspberrypi.org/forums/viewtopic.php?f=43&t=105100

stackoverflow.com

raspberrypi.stackexchange.com

morinezumiiii.hatenablog.com

Raspberry Piにカメラを接続する | テクニカルタイムアウト


最後に

秋月電子で買ったので、14日以内にちゃんと試しておけば交換してもらえたのかな・・・まあ勉強代と思って今回は諦めました。