2012年2月23日木曜日

[Android] アクションバーを画面下側から表示させてみました。アプリ編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

@adakodaさんが面白い事をしていらっしゃったのでインスパイア。
[Android] アクションバーを画面下側から表示させてみました
http://www.adakoda.com/adakoda/2012/02/android-47.html

アプリからでも出来るのでは?


アプリケーションではアプリを初期化する際にsetContentView()するんで、setContentView()で出来たView群が画面上の全てだと思いがちです。しかーし画面上に存在する以上タイトルバーやActionBarなども当然Viewの一種なわけです。ちゅーことはアプリケーションのトップレベルのViewを取れたらタイトルバーやActionBarも操作出来るのでは無いでしょうか!?

そもそもまずトップレベルのViewをアプリケーション側から取得する事が出来るのか。出来ます。ActicvityからWindowを取り出してgetDecorView()するだけ。
View root = activity.getWindow().getDecorView()

ではこのViewどういう構造になっているか。Viewツリーをダンプしてみましょう。ViewツリーのダンプはViewの階層構造をダンプするスニペットを参考に。
View v = getWindow().getDecorView();
Util.dumpViewTree(v, "");

吐かれたのがこれ。Android4.0.2のGalaxy Nexusで、プロジェクトはEclipse+ADTで生成して出来たものを全く触らずに利用。ターゲットは4.0です。

Util  D  com.android.internal.policy.impl.PhoneWindow$DecorView
Util  D   android.widget.LinearLayout
Util  D    com.android.internal.widget.ActionBarContainer
Util  D     com.android.internal.widget.ActionBarView
Util  D      android.widget.LinearLayout
Util  D       android.widget.ImageView
Util  D       android.widget.LinearLayout
Util  D        android.widget.TextView
Util  D        android.widget.TextView
Util  D      com.android.internal.widget.ActionBarView$HomeView
Util  D       android.widget.ImageView
Util  D       android.widget.ImageView
Util  D     com.android.internal.widget.ActionBarContextView
Util  D    android.widget.FrameLayout
Util  D     android.widget.LinearLayout
Util  D      android.widget.TextView
Util  D    com.android.internal.widget.ActionBarContainer

"com.android.internal.widget.ActionBarContainer"てのが居ますね。露骨ですね。なんで二個あるのかはちょっとわかりません。

findViewsWithClass()を拡張


じゃあ"com.android.internal.widget.ActionBarContainer"を取り出して操作したらいいんじゃまいか?どうやって取り出すの。ああ、何かそういうエントリを昨日見たよ。あった。List<T> findViewsWithClass(View v, Class<T>)。助かるわー。#ステマ

しかしどうもList<T> findViewsWithClass(View v, Class<T>)ではClass<T>を使っています。com.android.internal.widget.ActionBarContainerは名前しか判らないのでちょっと拡張する必要がありそうですね。

こうなった
public static <T extends View> List<T> findViewsWithClass(View v, Class<T> clazz) {
  List<T> views = new ArrayList<T>();
  findViewsWithClass(v, clazz.getName(), views);
  return views;
 }
 public static List<View> findViewsWithClassName(View v, String className) {
  List<View> views = new ArrayList<View>();
  findViewsWithClass(v, className, views);
  return views;
 }
 private static <T extends View> void findViewsWithClass(View v, String clazz, List<T> views) {
  if (v.getClass().getName().equals(clazz)) {
   views.add((T) v);
  }
  if (v instanceof ViewGroup) {
   ViewGroup g = (ViewGroup) v;
   for (int i = 0; i < g.getChildCount(); i++) {
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

これでcom.android.internal.widget.ActionBarContainerを簡単に取り出せます。

View root = activity.getWindow().getDecorView();
List<View> views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");



試しにsetVisibility(View.GONE)してみる


では試しに取り出したcom.android.internal.widget.ActionBarContainerをView.GONEで消してみましょう。

View root = activity.getWindow().getDecorView();
List<View>l views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");
for(View v : views){
  v.setVisibility(View.GONE);
}

消えたー!操作出来る。当たり前だけど。


actionBarUpsideDown(Activity activity)


Viewツリーのダンプを見ると、事実上ルートのコンテナはcom.android.internal.policy.impl.PhoneWindow$DecorView直下のLinearLayout。多分Vertical。つまり、この人にいるcom.android.internal.widget.ActionBarContainerをremoveView()で消して、addView()で足すとケツに移動するのではないか!?
という事でメソッドを作成。

public final static void actionBarUpsideDown(Activity activity) {
  View root = activity.getWindow().getDecorView();
  View firstChild = ((ViewGroup) root).getChildAt(0);
  if (firstChild instanceof ViewGroup) {
   ViewGroup viewGroup = (ViewGroup) firstChild;
   List<View> views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");
   if (!views.isEmpty()) {
    for (View vv : views) {
     viewGroup.removeView(vv);
    }
    for (View vv : views) {
     viewGroup.addView(vv);
    }
   }
  }
  else{
   Log.e(TAG, "first child is not ViewGroup.");
  }
 }

試す


メソッドにしたので試すのは簡単ですね

setContentView(R.layout.main);
Util.actionBarUpsideDown(this); 

キタワァ.*・゜゚・*:.。..。.:*・゜(n‘∀‘)η゚・*:.。. .。.:*・゜゚・*!!!!!☆



まとめ


ICS以外で呼ぶとどうなるか試してないので適宜何とかして下さい。
ActionBarが上に居るのは個人的に良く思っていないので、これ使って下に表示させる様にしていきたい。



Viewの階層構造をダンプするスニペット

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
まぁViewっていうかViewGroupですけど。

コード


こうです。あんまり階層が深いとスタックオーバーフローで死ぬと思います。
public static void dumpViewTree(View v, String padding){
  Log.d(TAG, padding + v.getClass().getName());
  if(v instanceof ViewGroup){
   ViewGroup g = (ViewGroup)v;
   for(int i = 0; i < g.getChildCount(); i++){
    dumpViewTree(g.getChildAt(i), padding+" ");
   }
  }
 }

実行

試しにandroid.support.v4.app.ListFragmentのonCreateView()で自動的に生成されるViewをダンプしてみました。 こういう構造してたんですねー。
27152 Util  D  android.widget.FrameLayout
 27152 Util  D   android.widget.LinearLayout
 27152 Util  D    android.widget.ProgressBar
 27152 Util  D   android.widget.FrameLayout
 27152 Util  D    android.widget.TextView
 27152 Util  D    android.widget.ListView

まとめ


デバッグとか動的にView書き換えてる場合とかAPIが勝手に生成するViewについて調べるのに使えるのではないでしょうかー。





2012年2月22日水曜日

List<T> findViewsWithClass(View v, Class<T>)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

android.view.ViewといえばfindViewById(int)ですよねー。
実はそれ以外にもfindViewWithTag (Object tag)やfindViewsWithText(ArrayList, CharSequence, int)てのがあるようです。
使い所がよくわかりませんがなんか使えそうですね。

個人的にはViewのクラス名で取り出したいなーと、例えばjavascriptのgetElementsByTagName(name)のノリだと言うと判ると思います。

List<T> findViewsWithClass(View v, Class<T>)


はい、完成。本当は v instanceof Tってやりたかったけど怒られました。仕方なくClassのフルネームで比較してます。なのでサブクラスとかまでは検出できません。

public static <T extends View> List<T> findViewsWithClass(View v, Class<T> clazz){
  List<T> views = new ArrayList<T>();
  findViewsWithClass(v, clazz, views);
  return views;
 }
 
 private static <T extends View> void findViewsWithClass(View v, Class<T> clazz, List<T> views){
  if(v.getClass().getName().equals(clazz.getName())){
   views.add((T)v);
  }
  if(v instanceof ViewGroup){
   ViewGroup g = (ViewGroup)v;
   for(int i = 0; i < g.getChildCount(); i++){
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

※追記20120223
@zaki50さんからツッコミが。ありがとうございます!!



直した!。これの場合だとサブクラスまでがっつり取れる。

 private static <T extends View> void findViewsWithClass(View v, Class<T> clazz, List<T> views) {
  if (clazz.isAssignableFrom(v.getClass())){
   views.add(clazz.cast(v));
  }
  if (v instanceof ViewGroup) {
   ViewGroup g = (ViewGroup) v;
   for (int i = 0; i < g.getChildCount(); i++) {
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

使う


こう使うとよさげ。例えばこういうレイアウト

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="mogeee" >
    </TextView>
    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" >
    </ImageView>
    <LinearLayout
        android:id="@+id/main_list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
    </LinearLayout>
</LinearLayout>

こういうコードで要素を取り出せます。以下はTextViewを取り出してます。

List<TextView> views = findViewsWithClass(v, TextView.class);
        if(views.isEmpty()){
         Log.d(TAG, "not found");
        }
        else{
         Log.d(TAG, "find:"+views.size());
        }

2012年2月20日月曜日

サポートパッケージのFragmentでstartActivityForResultを使う場合の注意点

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

ついにFragmentに入門したわけですが、早速ハマったのでメモしておきます。サポートパッケージェ・・・。
※本エントリはandroid.support.v4.app.Fragmentに関する動作について書いています。android.app.Fragmentでは以下に書いている問題は発生しません。

FragmentでstartActivityForResultが使える


Fragmentって便利ですねー。特にActivityにベッタリ書いていた様な実装をFragmentに分けて色々捗らせる事が出来る点がとても助かります。で、Activityに書いていた処理をFragmentに集約する時に問題になるのがstartActivityForResultによって処理を委譲している部分です。

FragmentではstartActivityやstartActivityForResultやonActivityResultが用意されており、FragmentからActivityを起動したり、結果を受け取る事ができます。

という事でまぁ問題なくねーと思いがちですがしかし、FragmentのstartActivityForResultには罠があるんですねーーーー!


リクエストコードは下位16bitの範囲で


まず以下のコードをFragment内で実行してみます。なんの変哲もない、アドレス帳を起動して選択結果を受け取るだけのIntentです。

private final static int REQUEST_CODE = 0x11111;
public void pick() {
 Intent intent = new Intent(Intent.ACTION_PICK, 
  ContactsContract.Contacts.CONTENT_URI);
 startActivityForResult(intent, REQUEST_CODE);
}

し、死んだー!

Can only use lower 16 bits for requestCode


なにーー!
そうですFragmentではstartActivityForResultの第二引数に与えるリクエストコードを下位16bit以内に収める必要があります。
つまり0x0000〜0xffffまでの間の値にしろ、という事ですね。上記コードは"0x11111"だったのでダメだったんですね。

リクエストコードは下位16bitの範囲で。



onActivityResultが呼ばれないパターン


こういう罠にはまるのは僕くらいのものな気がしますが、サポートパッケージのFragmentのstartActivityForResultではonActivityResultが呼ばれないパターンがあります。
親となるActivity側がonActivityResultをオーバーライドしている場合に起きるのですが・・・。
コードを見てみます。問題無いようなあるような・・・。

親となるActivity側
@Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data){
  switch (requestCode) {
  case REQUEST_CODE:
   //なにもしない
   break;
  case MOGE:
   //~処理~
   break;
  }
 }
Fragment側
@Override
 public void onActivityResult(int requestCode, int resultCode, Intent data) {
  switch (requestCode) {
  case REQUEST_CODE:
   if(resultCode == Activity.RESULT_OK){
    Log.d(TAG, "okkkk!");
   }
   else{
    Log.d(TAG, "ooooops!");
   }
   break;
  }
 }

このコードの場合Fragment側のonActivityResultが呼ばれません。何故か?

super.onActivityResultを忘れているから!


そうです。親Activity側のsuper.onActivityResultを呼んでませんでした。
サポートパッケージでFragmentを使う場合はActivityではなくFragmentActivityを継承する必要があります。
このFragmentActivityのonActivityResultでFragmentに結果を渡しているのでsuper.onActivityResultを呼んであげないとFragment側にonActivityResultの呼び出しが行かないんですねー。

正しくは
@Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data){
  super.onActivityResult(requestCode, resultCode, data);
  switch (requestCode) {
  case REQUEST_CODE:
   //なにもしない
   break;
  case MOGE:
   //~処理~
   break;
  }
 }

問答無用でsuper.onActivityResult。



まとめ


コンパーチビリティの為のサポートパッケージですがなかなかどうして、大変ですねー。


2012年2月11日土曜日

【Android】スムーズな開閉アニメーションを実現するExpandAnimatorというライブラリを作りました

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

デモ


まずは動作を。




この様に指定したViewの開閉を行う為のアニメーションライブラリを作りました。


ダウンロード


Androidのライブラリプロジェクトをgithubに公開しています。git cloneして下さい。

ExpandAnimator
https://github.com/sys1yagi/ExpandAnimator

構成は以下の通りです。


動画のデモはsampleの中に入っているサンプルプロジェクトを動作させたものです。


サポートバージョン


1.6〜


使い方


ExpandAnimatorの使い方は簡単です。

ライブラリプロジェクトを参照する


まずExpandAnimatorをライブラリプロジェクトとして参照します。



トリガとコンテナをlayoutXMLに定義する


開閉する対象となるViewと、開閉のトリガとなるボタンなどをlayoutXMLに書いておきます。
ここではボタンがトリガで色のついたLinearLayout部分がコンテナです。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

    <Button
        android:id="@+id/trigger"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="5" />

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp" >

        <View
            android:layout_width="fill_parent"
            android:layout_height="300dp"
            android:background="#ffccddff" >
        </View>
    </LinearLayout>

</LinearLayout>


ExpandAnimatorを使う


開閉アニメーションを行う為にExpandAnimatorクラスを利用します。
ExpandAnimatorは開閉対象となるViewを内部に持ってアニメーション制御を行います。

下記コードの20行目でExpandAnimatorをnewしています。引数に開閉対象となるViewを渡しています。
第二引数は開閉アニメーション時に呼び出されるリスナを指定します。

41行目でアニメーションの時間を指定しています。
42行目ではアニメーションの為のインタポレータをセットしています。DecelerateInterpolatorはだんだんアニメーションのスピードが減速していくインタポレータです。

43行目でトリガとなるボタンにOnClickListnerをセットし、onClick(View)でExpandAnimatorのexpand(), unexpand()をそれぞれ呼び出しています。

package test.test.test.test.test;

import jp.dip.sys1.yagi.android.expandanimator.ExpandAnimator;
import jp.dip.sys1.yagi.android.expandanimator.ExpandAnimator.OnAnimationListener;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;

public class ExpandSampleActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Button trigger = (Button) findViewById(R.id.trigger);
        //ExpandAnimatorを初期化する
        final ExpandAnimator animator = new ExpandAnimator(findViewById(R.id.container), new OnAnimationListener() {
            @Override
            public void onUnexpanded(ExpandAnimator e) {
                trigger.setText("onUnexpanded");
            }

            @Override
            public void onStartUnexpand(ExpandAnimator e) {
                trigger.setText("onStartUnexpand");
            }

            @Override
            public void onStartExpand(ExpandAnimator e) {
                trigger.setText("onStartExpand");
            }

            @Override
            public void onExpanded(ExpandAnimator e) {
                trigger.setText("onExpanded");
            }
        });
        //アニメーション時間を指定
        animator.setDuration(1000);
        //インタポレータを指定
        animator.setInterpolator(new DecelerateInterpolator());
        
        //トリガを設定
        trigger.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (animator.isExpand()) {
                    //閉じる
                    animator.unexpand();
                } else {
                    //開く
                    animator.expand();
                }
            }
        });
    }
}


出来た


こ、こいつ動くぞ・・・!



使いどころ


わかりません。


ライセンス


Apache License 2.0です。