Locked History Actions

Android/baconRank

Bacon Rank Android App

※これは以下のブログの翻訳である。

Bacon Rank Android App Details May 13, 2010, 10:11 pm

ベーコンランクはUIとバックグラウンド処理という観点から見て、僕が最も野心的に取り組んでいるプロジェクトだ。 これはカスタムなUIコントロールをもち、長時間のネットワークオペレーションをうまく処理する。

まずは、最初にカスタムなSeekBarを見てみよう。これは君がベーコンを何切れ食べたかを選択するものなんだ。 タッチスクリーンがあるなら、特に物理的なキーボードが無いのであれば、ユーザに何かをタイプさせるのは避けたいよね。 一回にそれほど多くのベーコンを食べる人なんていないから、このSeekBarはとてもうまく動作してくれるんんだ。 まずはカスタムなイメージでSeekBarを表示させるために、bacon_seekbar.xmlをdrawableフォルダに作成する。

<layer-list
  xmlns:android="http://schemas.android.com/apk/res/android"
>
  <item
    android:id="@+android:id/background"
    android:drawable="@drawable/progress_mediumbacon" />
  <item
    android:id="@+android:id/SecondaryProgress"
    android:drawable="@drawable/progress_rawbacon" />
  <item
    android:id="@+android:id/progress"
    android:drawable="@drawable/progress_cookedbacon" />
</layer-list>

progress_* というDrawableはPNG画像だよ。

メインレイアウトで、SeekBarを使い、カスタムなサムイメージを使う(skilletのPNG画像だ)。

<SeekBar
    android:id="@+id/SEEKBAR"
    android:layout_below="@id/BACON_STRIPS"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:max="20"
    android:progress="0"
    android:secondaryProgress="0"
    android:paddingLeft="32px"
    android:paddingRight="32px"
    android:progressDrawable="@drawable/bacon_seekbar"
    android:thumb="@drawable/skillet"
    />

SeekBarは三つの状態を持つ(バックグラウンドも入れるとするなら)、だけど二つしか必要ではない。 バックグラウンドと進捗状態を使おうとしたけど、おかしなことにうまく動かないんだ。 だからそのかわりに、SecondaryProgressをバックグラウンドとして使うことにした。バックグラウンドは見えないようにしてある。

onCreateでSeekbarをセットアップする。

mSeekBar = (SeekBar)findViewById(R.id.SEEKBAR);
mSeekBar.setOnSeekBarChangeListener(this);
mSeekBar.setMax(PROGRESS_MAX);

onProgressChangedメソッドで、UIの「量」を変更する。 とても素敵に見えるんだけど、でももっと3Dっぽくすればよりよくなるかも。

アプリケーションのもっともトリッキーな部分は、スクリーンの向きの変更中にネットワーク通信を扱う部分だ。 (Androidの)チュートリアルや、stackoverflowの質問と答えのどれもこれもうまくいかない(まぁ、僕が理解できていない可能性もあるけど)。

問題というのはこうだ。Activityの中でAsyncTaskを使ってネットワークとの通信を行うとしよう。 実際のところ、これはどこでだって推奨されてる方法だよね。

ところがだ。ユーザがスクリーンの向きを変えたらどうなる? デフォルトではAndroidは現在のActivityを破棄して、新しいActivityを作成する。 AsyncTaskは動作し続けていて、新しいActivityのことなんか知らないんだ。 逆に、新しいActivityはAsyncTaskのことなんか知らない。どうすりゃいいの?

そこで、カスタムなアプリケーションオブジェクトを作ればいいってわけ。 これはプロセスが走っている限り存在し続けるからね。 だから、ActivityがAsyncTaskをスタートさせたら、アプリケーションにそれへの参照を持たせる。 AsyncTaskはスクリーンに描画中のActivityへの参照が必要なんだけど、それを常に最新のものにするってわけ。

つまり、AsyncTaskの生成時にこれをセットし、スクリーンの回転時Activtyが破棄されたときにnullにし、Activityが再生成されたときに復帰する。 なおかつ、ネットワーク通信が進行中には進捗インジケータも表示させたいよね。 こんな感じだよ(関係の無い部分ば省略してるよ)。

public class BaconApp extends Application {
    public BaconRank.SyncTask mSyncTask = null;
 
    // ...
}
 
public class BaconRank extends Activity implements SeekBar.OnSeekBarChangeListener {
    // ...
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        // ...
    }
 
    BaconRank.SyncTask getSyncTask() {
        return ((BaconApp)getApplication()).mSyncTask;
    }
 
    void sync(String action) {
        ((BaconApp)getApplication()).mSyncTask = (SyncTask) new SyncTask(this).execute(getUserAgent(), mUserId,
                mBaconStrips.getText().toString(),
                action);
    }
 
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            syncTask.setActivity(null);
        }
    }
 
    @Override
    public void onRestoreInstanceState(Bundle inState) {
        super.onRestoreInstanceState(inState);
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            setProgressBarIndeterminateVisibility(true);
            syncTask.setActivity(this);
        }
    }
 
    @Override
    public void onStop() {
        super.onStop();
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            syncTask.setActivity(null);
        }
    }
 
    public class SyncTask extends AsyncTask<String, Void, JSONObject> {
        // ...
 
        public SyncTask(BaconRank activity) {
            super();
            mActivity = activity;
        }
 
        public void setActivity(BaconRank activity) {
            mActivity = activity;
        }
 
        protected void onPreExecute() {
            if (mActivity != null) {
                mActivity.setProgressBarIndeterminateVisibility(true);
            }
        }
 
        protected void onCancelled() {
            // mActivity cannot be null
            mActivity.setProgressBarIndeterminateVisibility(false);
            clearActivity();
        }
 
        void clearActivity() {
            // call in ui thread only
            ((BaconApp)mActivity.getApplication()).mSyncTask = null;            
        }
 
        protected void onPostExecute(JSONObject result) {
            if (mActivity != null) {
                // Update UI
            }
            clearActivity();
        }
 
        protected JSONObject doInBackground(String... params) {
            // Do the network stuff
        }
 
        // ...
 
    }

そうそう、カスタムアプリケーションオブジェクトをAndroidMnifest.xmlで定義するのを忘れないようにね。

  <application
    android:name=".BaconApp"
  >