SurfaceViewの描画処理入門

基本

キャンバスをロックして描画してキャンバスを返答することで描画するのが1セット

        // キャンバスを取得
        Canvas canvas = getHolder().lockCanvas();
        
        // 取得したCanvasで色々な描画処理
        
        // ロックを開放して描画
        getHolder().unlockCanvasAndPost(canvas);

描画の仕組み

SurfaceViewはViewと言いつつもWindowを作る。透明にする場合はWindowのTopに持ってくる必要があるため、SurfaceViewの上にViewを重ねることはできない。SurfaceViewの仕組みで上にViewを重ねたい場合はTextureViewを使えば良いらしい。詳しくは以下のリンクを参照のこと。

背景を透明にする

Formatを透明にするとできる。半透明でもよい

    getHolder().setFormat(PixelFormat.TRANSLUCENT);

アニメーション

Threadを作ってあげて頑張る

        Thread animThread = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean isAnimation
                while(isAnimation) {
                    // ここで基本通りcanvasLockして描画処理を書く
                    // アニメーション終わったらwhileループを抜ける
                }
            }
        });
        animThread.start();

ハードキーで音量を変更する

ハードキーで音量を変更しようとすると音を流していない状態だと着信音量が変更されてしまう。

メディアプレイヤーを作った場合、アプリが起動してる時はメディアの音量を変更したいのに着信音量が変更されてしまうのでそれでは使いにくい。

そういった場合は以下のコードを入れるだけでメディア音量を変更することができる。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // こいついれるだけでメディア音量に変わる
        setVolumeControlStream(AudioManager.STREAM_MUSIC);
    }

アプリ内でAndroidのテキスト読み上げ機能を使う

英語学習ソフトを作ったのだけど、どう読むのかわからなかったのでテキスト読み上げ機能を追加してみた。

使い方

テキストの読み上げにはTextToSpeechを使う

public class MainActivity extends BaseActivity {

    private TextToSpeech textToSpeech;

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

        // 第2引数はTextToSpeech.OnInitListener
        // 初期化が終わるとonInitが呼ばれる
        textToSpeech = new TextToSpeech(this, new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int status) {
                if (TextToSpeech.SUCCESS == status) {
                    // 今回は英語読み上げだから英語が使えるかを確認
                    if (textToSpeech.isLanguageAvailable(Locale.ENGLISH) >= TextToSpeech.LANG_AVAILABLE) {
                        // 英語が使えるなら読み上げ言語を英語にする
                        textToSpeech.setLanguage(Locale.ENGLISH);

                        // speakで読み上げ
                        textToSpeech.speak("word", textToSpeech.QUEUE_FLUSH, null);
                    }
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        // 終了時はtextToSpeechを終了させる
        // これやらないとエラーがでる
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
        super.onDestroy();
    }
}

まとめ

非常に簡単。ただ読み上げる声が電子音で怖い・・・。端末ごとに読み上げに使用する音声ファイルが違うので問題ない端末は人がしゃべっているように聞こえるから端末依存でしょうがないんだろうなぁ。単語の読み上げ機能が追加された「Swipe Card」。良かったら使ってね。

PCで作成したsqliteをAndroidのアプリ内にコピーする

読み込んで値を入れていくと遅いのでコピーしてDBファイルを作ったほうよい。isImportDatabase()でfalseが返答された場合dbが作成されてないので、importDatabaseでコピーする。この例の場合はassetsにフォルダにsqliteを入れているsqliteをコピーしている。

    private class DbHelper extends SQLiteOpenHelper {
        private static final String DB_FILE_NAME = "old_db.sqlite3";
        private static final String DB_NAME = "new_db.db";
        private static final int DB_VERSION = 1;

        private Context context;
        private File newDb;

        public DbHelper(Context context) {
            super(context, DB_NAME, null, DB_VERSION);
            this.context = context;
            this.newDb = context.getDatabasePath(DB_NAME);
        }

        public boolean isImportDatabase() {
            return newDb.exists();
        }

        public boolean importDatabase() {
            if (!newDb.exists()) {
                getWritableDatabase().close();
                try {
                    copy(context.getAssets().open(DB_FILE_NAME), new FileOutputStream(newDb));
                    getWritableDatabase().close();
                    return true;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }

        private int copy(InputStream input, OutputStream output) throws IOException {
            byte[] buffer = new byte[1024 * 4];
            int count = 0;
            int n = 0;
            while (-1 != (n = input.read(buffer))) {
                output.write(buffer, 0, n);
                count += n;
            }
            return count;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            super.onOpen(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }

androidannotations虎の巻

これぐらいわかれば使えそうだということをまとめてみた。

Android Studioでの環境設定

前に書いた。

xmlと変数とのヒモ付

以下のように簡単にかける。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imgView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/txtView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ListView
        android:id="@+id/lstView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

こういうxmlファイルが有った場合に

@EActivity(R.layout.activity_main)
public class MainAcitvity extends Acitvity {

    // R.idで指定できる
    @ViewById(R.id.imgView)
    protected ImageView imgView;

    // 指定を除外した場合は変数名=idとして紐付けられる
    @ViewById
    protected TextView txtView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @AfterViews
    protected void init() {
        // findViewByIdで@ViewByIdで指定された変数に値が入った後に
        // ここが呼ばれる。
        // onCreateで変数を使うとぬるぽで落ちるので注意
    }

    @Click(R.id.btn)
    protected void btnClick(View view) {
        // クリックイベントのバインドも簡単にできる。
        // 裏ではR.id.btnのsetOnClickListenerが呼ばれる
    }

    @ItemClick(R.id.lstView)
    protected void lstViewItemClick(MyItem clickedItem) {
        // リストViewのItemClickもできる
    }
}

こうかける。上はアクティビティの例だけど、Fragment(@EFragment)、ViewGroup(@EViewGroup)でも同じようなことが出来る。LinearLayoutとか作るときに便利。

AndroidManifest.xmlの書き方

acitivtyの指定は自動生成されるソースのファイル名を書く必要がある。そのためActivity名に_がついたものを書く必要がある。注意するのはそれぐらい。

    <!-- MainActivityが自動生成されるとMainActivity_となる -->
    <activity
        android:name=".activity.MainActivity_"
        android:screenOrientation="portrait" />

Background / UiThread

ちょっとした非同期実装ならアノテーションで書くことが出来る。EActivity等で使用することが出来る。

    @Background
    protected void getHttp() {
        // ここはバックグラウンドで実行される
        // Http通信とかよくやると思う
        String result = getHttp();
        finishHttp(result);
    }

    @UiThread
    protected void finishHttp(String result) {
        // ここはUIスレッドになる。
        // 画面操作等はここでやる。Toastを出すのも問題ない。
        Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
    }

Activityの起動

起動する場合は以下のように記載する


    new MainActivity_.IntentBuilder_(this).start();

結果が欲しい場合はstartForResultを使う。


    new MainActivity_.IntentBuilder_(this).startForResult(REQUEST_CODE);

Activityに値を渡したい場合は@Extraを使うと便利


@EActivity(R.layout.activity_main)
public class MainAcitvity extends Acitvity {

    // 型は色々指定することが出来る
    @Extra
    protected int extInt;
    @Extra
    protected String extString

こういう定義をした場合、呼び出し時に指定することが出来る


    new MainActivity_
        .IntentBuilder_(this)
        .extInt(intValue)
        .extString(stringValue)
        .start();


Fragmentのビルド

ビルダーを使ってビルドする

    SampleFragment_.builder().build();

値を渡したい場合は受け取り側で@FragmentArgで指定しておけば受け取れる。


@EFragment(R.layout.fragment_sample)
public class SampleFragment extends Fragment {

    // 型は色々指定することが出来る
    @FragmentArg
    protected int extInt;
    @FragmentArg
    protected String extString

こういう定義をした場合、呼び出し時に指定することが出来る


    SampleFragment_
        .builder()
        .extInt(intValue)
        .extString(stringValue)
        .build();

ViewGroupのビルド

ビルダーを使ってビルドする

    LinearLayoutView_.build(this);

値の設定とかできないから、publicメソッドでも作ってデータを紐付けすればいいと思う。

Preferenceの使用

プリファレンスもインターフェースを作るだけで簡単に触れる。Editorとかkeyとか指定しなくていいから簡単。

@SharedPref(value = SharedPref.Scope.UNIQUE)
public interface SharedPreferenceInf {

    // 関数として書く必要がある
    int intValue();
    String stringValue();

使う時の定義は@Prefで指定する


    @Pref
    protected SharedPreferenceInf_ pref;

値にアクセスするときはget()、put()を使用する


    // getで取得
    int intValue = pref.intValue().get();

    // putで編集
    pref.stringValue().put("hogehoge");


まとめ

これぐらいわかってれば色々捗ると思う。特に@Backgroundと@UiThreadは非常に便利。これを使うためにArrayAdapterが作成するViewをAndroidAnnotationsを使ってビルドする価値はあると思う。僕が使ってる時はprogardがうまく動かなかったけど、今のバージョンだと出来るようになってるのかなぁ。

ButterknifeでfindViewByIdを自動化する

Androidでxmlを書いてidつけてソースでfindViewByIdっていちいち書くのは非常にだるい。前に書いたandroidannotaionsだと重厚すぎるという場合にはButterknifeを使うと捗る

インポート

build.gradleのdependenciesに以下を記載

compile 'com.jakewharton:butterknife:6.0.0'

使い方

InjectViewアノテーションでひもづけるIdを指定してあげればいい

    @InjectView(R.id.txtView)
    protected TextView txtView;
    @InjectView(R.id.imgView)
    protected ImageView imgView;

クリックにリスナーを付けたい場合も簡単に書ける

    @OnClick(R.id.btn)
    protected void btnClick() {
        // ここがonClickで呼ばれる
    }

まとめ

findViewByIdの紐付けって毎回書くのがだるいのでこれ使うと楽にできるからいいと思う。ArrayAdapterのViewHolderを作る場合も使える。例は最初のURLに書いてあるから省くけど非常に簡単に書くことが出来る。AndroidAnnotationsだとProGardがうまく動かなかったり出来上がるものが重厚すぎるのでちょっとしたアプリならButterknifeのほうがおすすめ。是非使ってみてね。

Tinder風のUIを簡単に作れるライブラリ

SwipeCardsというライブラリが非常に便利

English Study Cardもこのライブラリを使って作っている

インポート

build.gradleのdependenciesに以下を追加してsync

compile 'com.lorentzos.swipecards:library:1.0.7@aar'

layout

    <com.lorentzos.flingswipe.SwipeFlingAdapterView
        android:id="@+id/frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

ソース

SwipeFlingAdapterViewにArrayAdapterを設定するだけで動く。リストをカスタムするのと同じようにArrayAdapterのgetViewを書き換えれば簡単にTinder風のUIが再現できる。拾える各種イベントは以下の様なもの。

ArrayAdapter adapter = new ArrayAdapter(this);
flingContainer.setFlingListener(new SwipeFlingAdapterView.onFlingListener() {
    @Override
    public void removeFirstObjectInAdapter() {
        // 一番上のカードがめくれた時に呼ばれる

        // 一番上のアイテムをここで入れ替えてあげる必要がある
        adapter.remove(adapter.getItem(0));
    }

    @Override
    public void onLeftCardExit(Object dataObject) {
        // 左にフリックされた時に呼ばれる
    }

    @Override
    public void onRightCardExit(Object dataObject) {
        // 右にフリックされた時に呼ばれる
    }

    @Override
    public void onAdapterAboutToEmpty(int itemsInAdapter) {
        // Adapterに入ってるアイテムが無くなった時に呼ばれる
    }
});

動き

動きとしてはカードがフリックされた時にremoveFirstObjectInAdapter()が呼び出されるので、ここで表示されるものを変更してあげる必要がある。例ではadapterの一番上のitemをremoveすることで次のやつが出てくるようにしている。

感想

すごく便利だけど、カードをフリックしている途中にどれだけ移動したかのイベントは存在しないため、移動しながら画面が変化するとか作ることができない。簡単なアプリなら使えるけど、複雑なアプリを作るなら自分で実装したほうが早いと思う。FlyingCardっていうのを簡単に使えるしたライブラリらしいのでそっちをカスタマイズしたらいいかも。

ListViewでタップした時の反応を消す

listSelectorに透明を設定すると反応していないように見える。

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="@android:color/transparent" />

ソフトウェアキーボードでレイアウトを変える

ちょっとめんどくさかったので覚書

やりたいこと

画面の下に常にボタンを表示なければならないのだが、画面には入力ボタンがあり画面全体の高さは変更してはいけない。高さを変更できないのでScrollViewは使えないので、FrameLayoutで全体を括ってソフトウェアキーボードが表示されたタイミングでボタンの位置をずらして画面下に表示しなければならない。

コード

frmWrapper.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        Rect r = new Rect();
        frmWrapper.getWindowVisibleDisplayFrame(r);
        int heightDiff = frmWrapper.getRootView().getHeight() - (r.bottom - r.top);
        lnrButtonWrapper.layout(
                lnrButtonWrapper.getLeft(),
                r.bottom - r.top - getSupportActionBar().getHeight() - lnrButtonWrapper.getHeight(),
                lnrButtonWrapper.getRight(),
                r.bottom - r.top - getSupportActionBar().getHeight());
    }
});

解説

全体を括っているFrameLayoutにLayoutListenerを追加する。こうすることで、ソフトウェアキーボードが表示されるとFrameLayoutの高さが変更されるのでこの中に入ってくる。getRootViewの高さを取得すると画面全体の高さが取得できるので、そこからアクションバー等の高さを引いてあげることで画面下にボタンが入っているLinearLayoutを表示することが出来る。

まとめ

ソフトウェアキーボードの表示、非表示を取得したいだけであれば、画面の高さから全体を括っている高さを引いた値が100を超えていたらソフトウェアキーボードが表示されていると判断していいらしい。Viewを拡張してonMeasureをOverrideする例が引っかかるけどそのためだけにカスタムビューを作るのは非常に面倒なのでこっちのほうがらくだと思う。

おまけ

出てるか出てないかの取得はこうやる

    DisplayMetrics displayMetrics = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    windowHeight = displayMetrics.heightPixels - getSupportActionBar().getHeight();
    lnrWrapper.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            Rect r = new Rect();
            lnrWrapper.getWindowVisibleDisplayFrame(r);
            if ((windowHeight - (r.bottom - r.top)) > 100) {
                // ソフトウェアキーボード出てる
            } else {
                // ソフトウェアキーボード出てない
            }
        }
    });

参考URL