手前と奥を別々の速度で動かして奥行き感を出すアレ(Parallax)をScrollViewで何とかしてみる

こんにちはこんにちは!
今日は「手前と奥を別々の速度で動かして奥行き感を出すアレ」をScrollViewでなんとか作ってみたいと思います。2Dゲームなんかで遠くの景色がゆっくり動いてく感じのアレです。WebなんかではParallaxとか読んだり呼ばなかったりすると聞きました。

完成図




そうだね、全然わからないね。
今回も練習がてらgithubにプロジェクト上げたので適当に試してみて下さい。
GitHub - ytRino/ParallaxScrollViewSample: ScrollViewを重ねてParallaxな感じに動かしてみる

素材

今回使う素材はこんなかんじです
めんどくさいのでとりあえずmdpiに置いて勝手にスケールして頂きます。
GitHub - res/drawable-mdpi
https://github.com/ytRino/ParallaxScrollViewSample/raw/master/res/drawable-mdpi/layer3.png
https://github.com/ytRino/ParallaxScrollViewSample/raw/master/res/drawable-mdpi/layer2.png
https://github.com/ytRino/ParallaxScrollViewSample/raw/master/res/drawable-mdpi/layer1.png
もうちょっとわかりやすい画像用意しろよ自分。

レイヤに手を加える

  1. 手前のレイヤ(ScrollView)から、後ろのレイヤを動かする
  2. 後ろのレイヤのボタンが息してない

の2項目にわけます。

手前のレイヤ(ScrollView)から、後ろのレイヤを動かす

HorizontalScrollViewに連動させるレイヤを設定するメソッドを追加し、手前のレイヤがスクロールした後に呼ばれるonScrollChangedで、設定したレイヤをスクロールさせます。このメソッドは短い時間に何度も呼び出されるので若干アレゲではあります。
ParallaxScrollView

    /**
     * このレイヤーと一緒に動かす背景を設定
     * 
     * @param psv このレイヤーと一緒に動かす背景. nullを渡すとそれまでaddしたレイヤーがクリアされる
     * @return this
     */
    public ParallaxScrollView addBackLayer(ParallaxScrollView psv, float ratio) {
        if(mBackLayers == null) {
            mBackLayers = new ArrayList<ParallaxScrollView>();
        }
        if(psv == null) {
            mBackLayers.clear();
        }else {
            mBackLayers.add(psv);
            psv.setScrollRatio(ratio);
        }
        return this;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        // 力技
        if(mBackLayers != null) {
            for(ParallaxScrollView hsv: mBackLayers) {
                hsv.scrollTo(l, 0);
            }
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo((int)(x * mRatio), y);
    }

    public void setScrollRatio(float ratio) {
        mRatio = ratio;
    }

setRadio()はそのスクロール位置の比率です。これで後ろはよりゆっくりになるように設定することで遠近感をだします。ParallaxActivity#getLayerWidthRatioで計算しています。
あとはActivity側で手前のレイヤに奥のレイヤを追従するようにします。

        mLayer1 = (ParallaxScrollView)findViewById(R.id.layer1);  // 手前
        mLayer2 = (ParallaxScrollView)findViewById(R.id.layer2);  // 奥
        mLayer3 = (ParallaxScrollView)findViewById(R.id.layer3);  // もっと奥
        // layer2,3を1に追従させる
        mLayer1.addBackLayer(mLayer2, ratio2).addBackLayer(mLayer3, ratio3);

これでとりあえず連動して動くようになりました。
しかし問題があります。各レイヤには画像の他にボタンをつけています。後ろのレイヤがただの飾りならこれでもいいのですがボタンが有ると話は別です。手前のレイヤ(スクロールビュー)が画面いっぱいに広がってるのでタッチイベントをすべて拾ってしまい、後ろのレイヤ、そこに載っているボタンはさっぱり反応しません。

後ろのレイヤのボタンが息してない

このままでは飾りにしかならないのでうまく動くようにしましょう。
とりあえず手前のレイヤのタッチイベントにフックして後ろのレイヤにタッチイベントをそのまま投げてしまいます。
ただし、単に送ると後ろのレイヤ(スクロールビュー)自身がタッチに反応してスクロールするようになってしまい困るので、それも制御します。
タッチイベントを後ろのレイヤに投げるには、手前のレイヤタッチイベントで後ろのレイヤにdispatchTouchEventすればよさそう。
さらにスクロールを無効にするには…スクロールはタッチイベントに連動して行われます。スクロールさせないようにするためには

  • ScrollView#onTouchEventをオーバーライドして無視する
  • ScrollView#setOnTouchListenerでリスナをセットし#onTouchでtrueを返す(ScrollView#onTouchEventが呼ばれなくなる)

どっちにしろリスナは使うのでこれもリスナでやってしまいます。

        // layer2にタッチを伝播
        mLayer1.setOnTouchListener(new OnTouchDispatcher(mLayer2, false));
        // layer3にタッチを伝播 layer2,3はタッチイベントでスクロールなどさせないようにするのでtrue
        mLayer2.setOnTouchListener(new OnTouchDispatcher(mLayer3, true));
        // タッチを伝播しない
        mLayer3.setOnTouchListener(new OnTouchDispatcher(null, true));

ParallaxScrollView$OnTouchDispatcher

    /**
     * タッチイベントを伝播する<br>
     * 
     */
    public static class OnTouchDispatcher implements View.OnTouchListener {

        /** イベントの伝播先 */
        private final View mTarget;
        /** trueなら自身のタッチイベントを抑止(スクロールなど) */
        private final boolean mConsume;

        /**
         * @param receiver イベントの伝播先 伝播させない場合はnull
         * @param consume trueでタッチイベントを消費する
         */
        public OnTouchDispatcher(View receiver, boolean consume) {
            mTarget = receiver;
            mConsume = consume;
        }

        @Override
        public boolean onTouch(View v, MotionEvent e) {
            if(mTarget != null) {
                mTarget.dispatchTouchEvent(e);
            }
            return mConsume;
        }
    }

完成!




え?カクカク?オーバーシュートする?(∩゚д゚)アーアーきこえなーい
ともかくこれで、「なんかこうさ〜画面が動く感じの導線がほしいよね〜そうそう手前と背景があって奥行き感ある感じ?キャラがボタンになっててさ〜」といったなんとも言えない要求に対応できますね!
いったいどこにこんな仕様のアプリを作りたい人がいるのかよくわかりませんが(ぇ
もちろんこれでゲームなんか作れませんのであしからず。
めんどくさいひとのためのAPK
GitHub - ytRino/ParallaxScrollViewSample: ScrollViewを重ねてParallaxな感じに動かしてみる

ビットマップをキャッシュする(Caching Bitmaps / Android Training - Displaying Bitmaps Efficiently)

ytRino2012-07-01

Original

Caching Bitmaps  |  Android Developers
追記(20120823):やんざむさんが補足付きで解説してます。->Y.A.M の 雑記帳: Android Bitmap をキャッシュする

Bitmapのキャッシュ

 ひとつのビットマップをUIに読み込むのは簡単ですが、もしあなたがたくさんの画像を一度に必要としている場合、複雑なことになります。
多くの場合(例えばListView,GridViewViewPagerといったコンポーネントとともに使用する場合など)、画面に組み合わされてすぐに画面上にスクロールするかもしれないような画像の総数は本質的に無制限です。*1
 これらのコンポーネントは、子ビューがスクリーンから見えなくなるとそれをリサイクルすることによってメモリ使用量を抑ています。
ガベージコレクターはまた、あなたがどんな長さの生きた参照も持たないと判断すると、ロードしたビットマップを開放します。これらの仕組みはとても良いのですが、なめらかで高速にロードするUIを維持するためには、画面に戻ってくるたびに何度もそれらの画像を処理するようなことはさけたいでしょう。
ここでメモリとディスクのキャッシュがしばし役に立ち、コンポーネントに、処理された画像を素早くロードすることを可能にします。
 このレッスンでは、複数のビットマップをロードするときに、メモリとディスクのキャッシュにより応答性と滑らかさを向上させる使い方を順を追って説明します。

メモリキャッシュを使う

 メモリキャッシュはアプリケーションの貴重なメモリを使うというコストを払って高速なビットマップアクセスを提供します。[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]クラス(SuppotLibraryによってAPIレベル4から利用可能)はビットマップのキャッシュ、[http://developer.android.com/reference/java/util/LinkedHashMap.html:title=LinkedHashMap]によって強参照されている直近のオブジェクトの保持、キャッシュがその容量を超過する前に最小限最近使用された中身を取り出す、といった用途に特に適しています。

Note: かつて[http://developer.android.com/reference/java/lang/ref/SoftReference.html:title=SoftReference][http://developer.android.com/reference/java/lang/ref/WeakReference.html:title=WeakReference]のビットマップキャッシュはポピュラーなメモリキャッシュの実装でしたが、現在では推奨されていません。Android2.3(APIレベル9)から、ガベージコレクターはより積極的にsoft/weak リファレンスを回収しこれらをとても効果のないものにします。それに加えてAndroid3.0(APIレベル11)以前は、背後のビットマップデータはネイティブメモリへストアされ、開放が予測できないためアプリケーションにたやすくメモリの限界を超過させ、クラッシュさせる原因となります。

 [http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]に適切なサイズを選ぶために、いくつかの要因を考慮するべきです。

  • あなたのアクティビティやアプリケーションで集中的に使えるメモリはどれくらい残っていますか?
  • 一度に画面上に表示される画像はどのくらいですか? どのくらい画面上に表示する準備ができている必要がありますか?
  • バイスのスクリーンサイズとdensityはいくつですか? Galaxy NexusのようなxhdpiのデバイスNexus Sのようなhdpiのデバイスと比べて、同じ数の画像をメモリに保持するのにもより大きなキャッシュを必要とします。
  • ビットマップの大きさと構成はどのようになっていますか? そして各々どれくらいのメモリを占有しますか?
  • 画像はどのくらいの頻度でアクセスされますか? いくつかの画像が他よりも頻繁にアクセスされますか? もしそうならばそれらのアイテムを常にメモリに持っておくか、さらに異なるビットマップに対して複数の[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]オブジェクトを持つと良いかもしれません。
  • 量と質のバランスをとることができますか? 多くの低品質のビットマップをストアしておき、バックグラウンドタスクで高品質版をロードすることが有効な場合があります。

特定のサイズやすべてのアプリケーションにあった式はなく、使い道を分析し適切な解決に導くのはあなた次第です。小さすぎるキャッシュは余計なオーバーヘッドが発生し利点がなく、大きすぎるキャッシュは再びjava.lang.OutOfMemory例外を発生させるかもしれず、あなたのアプリが動作するメモリを残り僅かにするでしょう。
 ビットマップのために[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]を用意する例です。

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // このデバイスのメモリクラスをゲットする。
    // この量を超えるとOutOfMemory例外が投げられる。
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // このメモリキャッシュから利用可能なメモリの1/8を使う
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

Note: この例では、アプリケーションメモリの8分の1がキャッシュに割り当てました。よくある、hdpiのデバイスでは最小約4MB(32/8)です。フルスクリーンの[http://developer.android.com/reference/android/widget/GridView.html:title=GridView]を画像で満たした800x480の解像度のデバイスでは1.5MB(800*480*4バイト)を使用します。よって少なくとも2.5ページ分ほどの画像をメモリにキャッシュできるでしょう。

 ビットマップを[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]にロードする時、[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]がはじめにチェックされます。もしエントリが見つかればすぐに[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]をアップデートするために使用され、そうでなければ画像を処理するためにバックグラウンドスレッドが開始されるでしょう。

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

 [http://developer.android.com/training/displaying-bitmaps/process-bitmap.html#BitmapWorkerTask:title=BitmapWorkerTask]もまた、メモリキャッシュにエントリを追加するよう更新する*2必要があります。

class BitmapWorkerTask extends AsyncTask {
    ...
    // 画像をバックグラウンドでデコード
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}
ディスクキャッシュを使う

 メモリキャッシュは最近表示したビットマップへのアクセスを高速にするのに役立ちますがこのキャッシュで利用可能な画像に頼ることはできません。[http://developer.android.com/reference/android/widget/GridView.html:title=GridView]のようなコンポーネントと大きなデータセットは簡単にメモリキャッシュを埋めてしまいます。あなたのアプリケーションは電話のような他のタスクによって中断され、バックグラウンドにいる間にkillされ、メモリキャッシュは破棄されてしまうかもしれません。ユーザが戻ってくると、アプリケーションはそれぞれの画像を再び処理しなければいけません。
 ディスクキャッシュはこれらのケースで、処理されたビットマップ存続させ、メモリキャッシュから利用できなくなった画像のロード時間を短くするのに使えます。
もちろん、ディスクからの画像の取得はメモリからロードするよりも遅く、読み込み時間が予測できないのでバックグラウンドスレッドで行われるべきです。

Note: 画像ギャラリーアプリケーションのように、画像に頻繁にアクセスする場合は[http://developer.android.com/reference/android/content/ContentProvider.html:title=ContentProvider]がより適切なキャッシュ画像の格納場所となるでしょう。

 サンプルに含まれるこのクラスは基本的なDiskLruCacheの実装です、しかし、より強固で推奨されるDiskLruCacheのソリューションはAndroid4.0のソースコードに含まれています(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)。過去のAndroidで使用するためにバックポートするのはとても簡単です(検索すればこのソリューションをすでに実装しているほかの人を見つけることができます)。
 シンプルなDiskLruCacheを使用してアップデートされたコードです。

private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // メモリキャッシュを初期化
    ...
    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
    ...
}

class BitmapWorkerTask extends AsyncTask {
    ...
    // バックグラウンドでデコード
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // バックグラウンドでディスクキャッシュをチェック
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // ディスクキャッシュになかった
            // 通常通りデコード処理をする
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 最終的なビットマップをキャッシュに追加
        addBitmapToCache(String.valueOf(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // はじめにメモリキャッシュに追加する
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // ディスクキャッシュにも追加する
    if (!mDiskCache.containsKey(key)) {
        mDiskCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    return mDiskCache.get(key);
}

// 指定されたアプリのキャッシュディレクトリを作成します。外部ストレージの使用を試みます。
// マウントされていなければ内部ストレージにフォールバック
public static File getCacheDir(Context context, String uniqueName) {
    // メディアがマウントされているかストレージ内蔵されているかチェックする。もしそうならば外部キャッシュディレクトリを使用するよう試みる
    // そうでなければ内部キャッシュディレクトリを使う
    final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
            || !Environment.isExternalStorageRemovable() ?
                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

 メモリキャッシュはUIスレッドでチェックされますが、ディスクキャッシュは、バックグラウンドスレッドでチェックされます。ディスクオペレーションはUIスレッドで行なうべきではありません。画像の処理が終わると最終的なビットマップは将来使うためにメモリとディスクの両方のキャッシュに加えられます。

構成の変化(Configuration Changes)を扱う*3

 スクリーンの回転などの実行時の構成の変化はAndroidにアクティビティを破棄させ、新しい構成で再び実行させます(この振る舞いについて詳細はHandling Runtime Changesを見て下さい)。あなたは、構成の変化が発生した時にユーザがスムーズで高速な体験を得られるように、すべての画像を再び処理するようなことは避けたいと考えるでしょう。
 幸いなことに、あなたにはメモリキャッシュを使うセクションで構築したビットマップのメモリキャシュがあります。このキャッシュは新しいアクティビティにsetRetainInstance(true)を呼ぶことによって保持される[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]を使って受け渡すことができます。
アクティビティが再び生成された後、この保持(retain)された[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]は再びアクティビティにアタッチされ、キャッシュオブジェクトにアクセスできるようになり、画像を素早く取得し[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]に設定することが可能になります。
 [http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]を使用して、構成の変更にまたがって[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]オブジェクトを保持する例です。

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache(cacheSize) {
            ... // ここで通常通り初期化する
        }
        mRetainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

 これをテストするには、[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]の保持(retain)をあり・なしでデバイスを回転させて下さい。キャッシュを保持した場合、画像がアクティビティにすぐに読み込まれてラグがほとんどないことに気がつくでしょう。
メモリキャッシュに見つからなかった画像は、うまく行けばディスクキャッシュから利用可能で、そうでなければ通常通り処理されます。

以上

キャッシュ初心者なんだけどどう実装すればいいんだろうと思って調べていたらトレーニングの項目にちょうどいい物があった。恥ずかしながらトレーニングって全然呼んだことなかったし、ついでなので英語のトレーニングも兼ねて(?)訳して見ました。
が、なんとなく理解できても改めて日本語に訳しにくかったり遠回しに感じられたりと散々だったので次に翻訳でも、という気になった場合は超意訳要約エントリにしようと思う…。
IS01 2周年バンザイ!

*1:どの画像もすぐ表示される可能性がある、みたいな?

*2:前章の続きなので

*3:Handle Configuration Changes なんて訳せばいいんですかこれ…

LayerDrawableの落とし穴

ytRino2012-01-28

LayerDrawable

こんにちはこんにちは!
みなさん、突然ですがLayerDrawable使ってますか?

   △  ¥ ▲
  ( ? 皿 ?)  がしゃーん
LayerDrawableだよ
自動でDrawableを重ねてくれるすごいやつだよ

LayerList - Drawable Resources | Android Developers

使ってみる

写真を添付するようなアプリをイメージして、
取り消し用の×印と添付する画像を2つほど用意して画像を切り替えてみました。
n1.png
n2.png
x.png

test_layer.xml 上に書いたものから置かれるので下にあるものが上に来ます。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/number">
        <bitmap
            android:src="@drawable/n1"
            android:gravity="center" />
    </item>
    <item>
        <bitmap
            android:src="@drawable/x"
            android:gravity="bottom|right" />
    </item>
</layer-list>

これをImageViewに設定するだけ。

    <ImageView
        android:id="@+id/image"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"

        android:src="@drawable/test_layer"
        />

楽ちんですね。
LayerDrawable#setDrawableByLayerIdで先ほどののIDと新しくセットするDrawableを指定して入れ替えます。

       LayerDrawable mLayer = (LayerDrawable)((ImageView)findViewById(R.id.image)).getDrawable();
       ...
       // 画像URIから適当にDrawable生成したりして
        Drawable drawable = getNewDrawable();
       // 入れ替え
        mLayer.setDrawableByLayerId(R.id.number, drawable);
       // 再描画
        mLayer.invalidateSelf();

単純に添付取り消しを表現するなら mLayer.setVisible(View.INVISIBLE)でいい気がする。

問題発生

こんな感じで順調に行くかとおもいきや、問題が。
Galaxy Nexusだとちゃんと切り替わるのに、他のいくつかの端末では切り替えて再描画の要求をだすと消えます。×印を残して。
結論から言うとICSでDrawbleがちょいちょい変わっていて、#setDrawableByLayerIdが(少なくとも自分の)思うような動作に修正されていました。
2.3の該当箇所
http://tools.oesf.biz/android-2.3_r1.0/xref/frameworks/base/graphics/java/android/graphics/drawable/LayerDrawable.java

    266     public boolean setDrawableByLayerId(int id, Drawable drawable) {
    267         final ChildDrawable[] layers = mLayerState.mChildren;
    268 
    269         for (int i = mLayerState.mNum - 1; i >= 0; i--) {
    270             if (layers[i].mId == id) {
    271                 layers[i].mDrawable = drawable;
    272                 return true;
    273             }
    274         }
    275 
    276         return false;
    277     }

シンプルですね。つづいでICS
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/graphics/java/android/graphics/drawable/LayerDrawable.java

    278     public boolean setDrawableByLayerId(int id, Drawable drawable) {
    279         final ChildDrawable[] layers = mLayerState.mChildren;
    280 
    281         for (int i = mLayerState.mNum - 1; i >= 0; i--) {
    282             if (layers[i].mId == id) {
    283                 if (layers[i].mDrawable != null) {
    284                     if (drawable != null) {
    285                         Rect bounds = layers[i].mDrawable.getBounds();
    286                         drawable.setBounds(bounds);
    287                     }
    288                     layers[i].mDrawable.setCallback(null);
    289                 }
    290                 if (drawable != null) {
    291                     drawable.setCallback(this);
    292                 }
    293                 layers[i].mDrawable = drawable;
    294                 return true;
    295             }
    296         }
    297 
    298         return false;
    299     }

2倍ぐらいに増えてる… 285,286、怪しさ満点です。さっきのコードのsetDrawableByLayerIdの前に付け加えてみます。

       // 画像URIから適当にDrawable生成したりして
        Drawable drawable = getNewDrawable();
       // 入れ替える前rのDrawableからbounds取ってきてセット
        Rect bounds = mLayer.findDrawableByLayerId(R.id.number).getBounds();
        drawable.setBounds(bounds);
       // 入れ替え
        mLayer.setDrawableByLayerId(R.id.number, drawable);
       // 再描画
        mLayer.invalidateSelf();

無事

動きました。
コード貼る所が無いので出来上がったAPKを置いておきます。
とりあえず見てみるかという方はどうぞ。apk
上のDrawableがそのままの処理で、下のDrawableがRectを設定するようにしたものです。GNだとどっちも同じように動き、2.3以前だと上は最初にボタンを押したときに消えてそのまま。IS01で動かしたらImageViewの大きさがばらばらになりました。

追記

あ、そういえばせっかくなのでgithubで公開してみました。
使い方全然わかりません\(^o^)/
https://github.com/ytRino/LayerDrawableSample

僕はGalaxy Nexusを手に入れることが出来たよ、あるいは独自画像でのローディング表現(Android Advent Calendar 2011 14日目)

14日目です><

こんばんはこんばんは!Android Advent Calendar 2011 14日目担当のytRinoです。

昨日のそばちゃんこさんのエントリを受け、「ぉяё маU〃 ゃレ£〃ぁωU〃ゃЙё?*1」状態であります。

そうそうたるメンバーが面白い記事を書いてる中、爆笑ネタも黒魔術ももってない僕は何を書こうか迷ったのですが、
以前ちらっとTLであいこんぐるぐるがーって話を見かけたときに「ですかね?」などと軽く答えてしまったものの自分のアプリ見直したら全然別物使ってたのでそれの紹介をしたいと思います。
本当は「Androidアプリ開発に役立つ10のテクニック(キリッ」とかやりたかったんですが無理でした XD

自分で作ったローディングアイコンをぐるぐるさせたい

ここで言うローディングはIndeterminateなローディングです。つまり進行度の表現がないやつです。

標準で使えるぐるぐるもいいけどせっかくなので自分で作った画像を回してみます。
用意するもの:

  • 回りそうな画像(透過png)
  • 設定XML
画像

今回は自分のアプリでも使ってる陰陽玉をつかいます。
よく回りそうですね。
この画像は /res/drawable/ic_dialog_onmyou.png などと適当な名前をつけて放り込んでおきます。

アニメーションさせるためのリソース

簡単なアニメーションはXMLで設定できます。
回転させるためのアニメーションにというのがあるようですがここではそれは置いといて別の方法を使ってみます。

animated-rotate

animated-rotateです。アニメーションではなくみんな大好きDrawableです。
android:pivot[X|Y]で画像の回転軸を設定します。%指定が使えるので中心指定も楽ちんです。

android:drawableで回転させるDrawable(画像)を指定します。

<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_dialog_onmyou"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="100"
 />

こいつ自体もDrawableなので/res/drawable/load_spin.xml などと保存しておきます。

あとはProgressBarやProgressDialogでsetIndeterminateDrawable()にこのDrawableを突っ込んであげましょう。

	private ProgressDialog dialog;
	dialog = new ProgressDialog(MyActivity.this);
	dialog.setIndeterminateDrawable(getResources().getDrawable(R.drawable.load_spin)); // さっきのDrawableを指定
	dialog.setCancelable(false);  //ただ試す場合は付けないように
	dialog.setMessage(getResources().getText(R.string.nowloading));
	dialog.show();

よく回ってます。確実に。
しかしandroid:durationが反映されてない気がするのは気のせいでしょうか。

ちなみにSDKに入ってたXMLにはandroid:framesCountという属性もあったり、Eclipseでもandroid:framesDurationとともに候補が出たりするのですがうまくいかなかったので削ってます。謎が多い。

他に書く予定候補だったもの

Android初級者が送るシリーズ

ListViewの各itemに横幅フィットさせて縦横比維持の縦クロップな画像を設定する
SQL初心者が初心者に送るAndroidSQL
AsyncTaskとネットワーク通信
BroadcastIntentキャッチした話
AIDLとサービスで玄人気分

無理でしたXD

おまけ

ドゥーコモゥ ギャアクシィネェクスゥス

そばちゃんこさんすいません、でも僕には稟議を通す相手がまず欲しいです。
というわけで12月2日、Galaxy Nexusを買いました。HT-03Aからの機種変で。
HT-03Aからの機種変で。1台持ち標準ROMで使っていたHT-03Aからの機種変で。

ドヤァ…
まあ正しくはメガネケースことIS01があるんですが、もうよくここまで使ってこれたなと誇りにすら思います。
もうぎゃらねくさんからは戻れない…


と言いたいところですがHT-03AがGalaxy Nexusに勝るいいところがあります。

  • 程よい大きさ(最近の機種はどれもでかすぎ ray欲しい)
  • トラックボール(全く見かけないんだがどういうことですか)

特にトラックボール、なぜこれほどまでに見かけないのでしょうか。文字入力の編集やアプリ/ブラウザの操作にも大活躍なのに…
苦労してリストやらselectorでfocusedの対応に悩んだこともあったのに…
何が言いたいかというと、トラックボール付き機種出してくださいお願いします!

Google「これからはソフトキー前提だわカーッ」
\(^o^)/

最後に

今年ついにAndroidでもAdventCalenderが行われました。
どうもほかの技術系Adventとは空気が違うような気がしないでもないですがそれもAndroidっぽいってことで一つ。
発起人のようてんさん@youten_redo に感謝!


そして今日(昨日?)の大ニュースといえばAndorid向け日本語入力のSimejiをバイドゥが買収 | TechCrunch Japanですね。
あだむさん@adamrocker、矢野さん@yanorinおめでとうございます!
HT-03Aのカバーの裏にはSimejiシールが貼ってあるしぎゃらねくさんになった今でもSimejiで入力してます。
HT-03Aでは「おい、いまキーボード呼んでない!」ということもありましたがやっぱりSimeji Love。
今までもこれからもありがとうございます。
GDD打ち上げで「パスポート取ったほうがいいよ」とあだむさんに言われたから11/1はパスポート取ったほうがいいよ記念日



最後に自己紹介
本エントリをご覧いただきまして、ありがとうございます。
@ytRinoっていいます。「Uたろう」って呼んでください。 うそです、「いいの」の方が落ち着きます。
大学生です。来年の春に卒業します。うそです、まだできません。
Android好きです。Java好きです。プログラミング大好きです。
デ部と町田支部と酒部には行ってるかもしれないので、僕を見かけたら声でも石でも投げてください。
どうも、ありがとうございました〜(*´▽`*)*2

Android Advent Calendar 2011、
今日の裏エントリは黒えあさん@kuroair 黒えあ-らぼ-: Androidとセキュリティ です!
明日の表エントリは、すますさん@ryosms です!
#もはや何が表で何が裏なのかさっぱりわからない

*1:訳:おれまじやばいんじゃね? id:sobachanko:20100806:1281076011

*2:via id:ngsw_taro:20111202:1322844344

Google Developer Day2011

#GDD11jp

11/1 Google Developer Day 2011 Japan @パシフィコ横浜
GDD11jp参加者の皆さん、スタッフの皆さんお疲れ様でした。
自分は当初DevQuizもやってないし優先枠とかも考えてなかったのでもとより参加しないつもりだったのですか、ボランティアでちゃっかり参加してしまいました。

当日はほとんどラウンジでOpencall担当としてのんびりしてました。kabayanさん持参の風船をそのへんの人につけたりChrome PCいじったり。

参加して何が良かったか
って言えばいろんな人と会えたこと。これにつきます。…まあ自由時間もあったのにセッション全然見てないから、セッションの内容で語れないってのもあるんですが。
自分がこんなことを言えるのも、「ガチで会いに行く」のではなくて「ちょうどいたのでちょっと話してみた」ぐらいの緩さだからこそだと思います。人見知りするのでそれぐらいの緩さじゃないと死んじゃうXD。 就活で「人脈出来ました(キリッ)」とかそういうことをするために行ってるんじゃないのでね。*1
というわけで今後もゆるーく色々イベントや勉強会など参加したいです。

終わった後は
スタッフ打ち上げ。しかしもともとsekitobaさん*2の計らいにより(?)非公式懇親会だけは参加するつもりだったので、ちょっと飲んでから勢いでadamrocker*3さんに突撃したあとダッシュで非公式の方へ移動しましたとさ。たぶんいろんな人に会えて感動っていうのはこの非公式懇親会が一番やばかったような気がします。うん、やばい。TLでしか知らない人が目の前にたくさん。こわいぇ
あと、自分のアプリ知ってますよ、って言われるとやっぱり嬉しい。

というわけで
みなさん、今後とも宜しくお願いします。

# はてブロに試し書きした記事をちょこっと修正してこっちにも投下しておきます。

*1:それはわかるんだけど…という違和感についてはいずれ。

*2:http://netyougo.com/slang/2770.html

*3:言わずもがなSimejiの開発者です

幻想郷きのこ畑が1万件突破した話

ytRino2011-08-15

世ではコミケというイベントが終了したわけですが皆さんいかがでしたでしょうか?
僕にとっては特に関係のないイベントでした。
嘘です。こじつけると関係なくもないです。
東方神霊廟でましたね。…わぁ妖夢がイメチェンしてr
これとどう関係あるかといえば僕が幻想郷きのこ畑というAndroid用アプリケーションを公開しているからという程度のことなんですが。

なんとこのたび、幻想郷きのこ畑は10000ダウンロードを突破しましたーというありがたい報告です。ついに「ダウンロード 10000-50000」ランクになりました。
いや、別にダウンロード数を稼いだりするのが目的ではないんですが。でもやっぱり嬉しいもんですね。ありがとうございます。
「使い方ワカンネー」「東方辞書あるのでいらね」「이거 어떻게 하는겁니까;; 」などいろいろ言われたりもしましたがやっぱり嬉しいですね。
アクティブ率が下がっていったりもしますがやっぱりうr

...
悲しい話はさておき、最新作が出た以上新キャラやらスペルカードの更新がしたいわけです。
なんでそんなことをわざわざ言ったのかというと、言わないとどんどん先延ばしにしてしまいそうだからです。
でもぶっちゃけOSいれかえたりした関係でやる前から怪しい雲行きが…


ちなみにどんなアプリかというのはこっちの紹介ページへどうぞ。
簡単にいえばAndroidで東方のあんなキャラ名やあんなスペルが簡単に入力できるという、それだけです。よければインストールしてみて、ついでに意見ください。よろしくお願いします。
そしてこのアプリにキャラ画像提供してもいいという方待ってます。切実に…
ではまた数カ月後に ぉぃぉぃ


# この前のエントリーからABC2011sに参加した話とかHT-03A卒業式に参加した話とか坂本真綾のライブでとんでもない目にあった話とか坂本真綾が結婚しちゃったとかなんか書くことあったはずなんだけどあれ…?

#app10 生放送に行ってきたよ

app10

id:taroid に誘ってもらったのでこれは面白そう、と行ってきました!
どういう進行になるのか全く知らなかったんですが、蓋を開けてみれば酒飲みながらワイワイやる放送でした。
以前の町田支部会のときの参加メンバーはじめ、あの人やらあのひとやら(自分の中で)有名人もいたりして面白いイベントだったと思います。相変わらずビビってしまいますね
ぶっちゃけANNは普段聴いてないのでリスナーにどう映ったのかはわからないけどこういうのもありかな、と。

(事前にタイトルコール練習

(腹筋アプリを試してるとこ

アンケートの「作ったアプリ」に「ライブ◯アのサービスをつかうための」って書いたけどがっつり「ライブドアねとらじ用アプリ」とか書いても良かったかもしれない…!全体的にチキンプレイをどげんかせんといかん
http://www.ustream.tv/recorded/14353572 1:03:30あたりから一瞬。

なんにせよ久しぶりにイベント行った気がして良かったかな。たろうさんありがとうございました!

宣伝:https://market.android.com/details?id=net.nessness.android.asteroid
最近競合アプリに置いてかrまあtuboroidの件と比較するのは失礼ですけどこれは完全に「やってみました」という感じなのでみんなのりk…というのはまた別の話
いちおうアンケートにはこっちhttps://market.android.com/details?id=net.nessness.android.thkinoko も書いたんですけど特にありませんでした

次なんか作るなら人に説明しやすくて一般向けなのがいいな…