satohu20xx's diary

思ったことをつらつらと

webviewを使ってWebアプリを作成する際に必要な設定まとめ

最近JavaScriptがはやってる
この流行でNativeとWebアプリのハイブリッドアプリを作成することが多くなろうからそういうときに使うWebViewの設定をまとめてみる。

JavaScriptを使用可能にする

これがないと始まらない

    webView.getSettings().setJavaScriptEnabled(true);

plugin使用可能

Flashとか使うときに必要

    webView.getSettings().setPluginsEnabled(true);

キャッシュを使用しない

開発の最中はキャッシュをOFFにしないと変わらない場合があるからわかりにくい

    webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

バックキーで戻る

これをしないとActivityが閉じてしまう。閉じてもいい場合は設定する必要ないけど。

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if(keyCode == KeyEvent.KEYCODE_BACK){
            if(webView.canGoBack() ) {
                webview.goBack();
                return false;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

Linkを同一WebViewで開く

WebViewClientを設定してあげれば同一WebViewで開ける

    webView.setWebViewClient(new WebViewClient());

mailtoとかmarketとかを別アプリで開く

このままだとリンクはすべてWebViewで開こうとしてしまうからhttpとhttp以外は別ActivityにIntentを投げる

    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url)
    {
        if(url.startsWith("http:") || url.startsWith("https:")) {
            return false;
        }

        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
        return true;
    }

まとめ

ほかにもぱっと浮かぶものとしてActivityとJavaScritの連携とかあるけど、それはいろんなところに書いてあるからそっちを参照してほしい。これぐらいやっておけばAndroidで簡易的なブラウザとして動くんじゃないかな。

忘れてることがもしあったら追記します。
ほかにもこんな設定あったらってのがあったら教えてくださいな。

Android Marketに自作アプリを公開してみた

コツコツ作っていたAndroidアプリがやっと完成したんで公開しました。

Youtubeで音楽を聞きながらバスに乗ってることが多かったんだけど、Youtubeの正式アプリがバックグラウンドで起動してくれないから、音楽を聞きながらブラウジングとかができない。それが不満でAndroid Marketを探してみたんだけど良いアプリがなかったからないなら作ってしまえってことでできたのがこのアプリ。

使い方の詳細とかは以下を参照してくださいな。

Radiotube

アプリを作った総評

動き的にはすごく単純なアプリで、やってることはYoutubeAPI使って動画用のURLを取得してそれをMediaControlに食わせてバックグラウンドで流すだけなんだけど、これが結構難しかった。SWFをMediaControlに食わせても動かなくて、formatにRSTPプロトコル用のURLが乗ってたからそれを食わせると今度は流れるけどすごく音質が悪い。。。

しょうがないからServiceがWebViewを作成してそれにSWFを食わせて動かしてJavaScriptでSWFを操作して、操作はActivityからServiceに対しての通信をさせて・・・みたいに結構面倒

紆余曲折あってなんとか形にはなったし、ServieとActivityの通信とかNotificationを使用したStatus Barへの表示とかsqlite使って音楽を管理とかひと通りの機能を使えてよかった。とりあえず公開するのが大事だと自分を慰める!

まとめ

まだ自分の実機(Xperia Arc)でしか動かしてないからいろいろ不具合がありそう使ってみてもし不具合があったら教えてください。極力直します。

次はカメラとか傾き操作とかハードのセンサー系を使ったアプリを作りたいなー

Status Barの値を更新してもgetExtra()で取得できる値が変わらない

NotificationManagerを使用してStatus Barに表示する値は変わるけど、getExtra()で渡ってくる値が変わらないのですごく悩んだ。

変更出来なかったコードは以下。

PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

これで更新するとStatus Barの値とか通知バーの値が変わるからこれでgetExtra()で渡ってくる値も変わるかと思ったけど、これだとPendingIntentが更新されない。どうも第4引数で渡すフラグが原因みたい。
第4引数のFlagで渡せる値には以下のものがある。

  • LAG_CANCEL_CURRENT(0x10000000):記述された PendingIntent がすでに存在していた場合、新しいのが生成される前に現在のはキャンセルされる
  • FLAG_NO_CREATE(0x20000000):記述された PendingIntent がまだ存在していない場合、生成せずに単に null を返す。
  • FLAG_ONE_SHOT(0x40000000):この PendingIntent は1度だけ使える。このフラグがセットされた場合、send() が呼ばれた後に試みた send は自動的にキャンセルされる。
  • FLAG_UPDATE_CURRENT(0x08000000):記述された PendingIntent がすでに存在している場合、それをキープして extra data を新しい Intent のものに置き換える。

ってことで、変更出来なかったコードで渡している0って言うものはない。だから、Flagのなにが使用されているのかがわからない。挙動からみるとデフォルト値はFLAG_NO_CREATEが設定されていると思われる。このせいで新たにPendingIntentが生成されずに最初に作成したPendingIntentが使用されるのでgetExtra()で取得される値が変わらないって仕組み。

なので、以下のコードに書き換える。

PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

これで正常に渡される。

ちなみに第2引数はrequestCodeってなっている。同一のアプリケーションで複数のPendingIntentを使用する場合はこの値を変えることで複数のPendingIntentを作成することができるみたい。

やっぱりコードのコピペをするんじゃなくて引数全てをきちんと調べる必要があるよなー

android-support-v4を使ってスワイプする

スワイプ動作をやるために必要なこと

サンプル的なコード

android-support-v4.jarを取ってくる

以下の場所にあるからコピーして場所を移してimportするなりそのままでimportするなりしましょう

android-sdk-windows\extras\android\compatibility\v4

ViewPagerを含んだLayoutファイルを作成
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

  <android.support.v4.view.ViewPager
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:id="@+id/viewpager"/>

</LinearLayout>
FragmentActivityを作成
public class TopFragmentActivity extends FragmentActivity {

    private ViewPager mViewPager;
    private TopViewPagerAdapter mPagerAdapter;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);

        mPagerAdapter = new TopViewPagerAdapter(getSupportFragmentManager());
        mViewPager = (ViewPager) findViewById(R.id.viewpager);
        mViewPager.setAdapter(mPagerAdapter);
    }
}
ViewPagerAdapterを作成
public class TopViewPagerAdapter extends FragmentPagerAdapter {

    // こいつがページ数
    private static final int PAGE_NUM = 2;

    public TopViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment = null;

        /*
         * このpositionに表示するViewの番号が来る
         * 初期表示は0で右にスワイプするごとにインクリメントみたいな
         * 毎回newせずにコンストラクタでnewして渡すほうがいいはず
         */
        fragment = new TopFragment();

        return fragment;
    }

    /*
     * こいつの返却数がページ数になる
     */
    @Override
    public int getCount() {
        return PAGE_NUM;
    }
}
Fragmentを作成する
public class TopFragment extends Fragment {

    private Button button;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.play_list_select, container, false);
        Activity act = getActivity();

        // こんな感じでLayoutファイルのViewを取ってくる
        button= (Button)v.findViewById(R.id.button);

        /*
         * あとは好きに書いてくださいなー
         */

        return v;
    }
}

あとがき

こんな感じでスワイプ動作ができるようになるはず。
やっぱりスマホならスワイプ動作が使いやすいよね。

ConvertViewの再利用の解釈

よくAdapterを使ったときにConvertViewの再利用がどーだこーだ書いてあるけど、解釈したことのメモ

ListViewを例にとって話をする。

    class ListAdapter extends ArrayAdapter<String>{
        public ListAdapter(Context context, String[] str) {
            super(context, 0, str);
        }

        public View getView(final int position, View convertView, ViewGroup parent) {

            if(convertView == null) {
                convertView = new TextView(getContext());
                ((TextView)convertView).setTextSize(32.f);
                convertView.setTag(position);
            }

            ((TextView)convertView).setText(getItem(position));

            Log.d("DEBUG", String.valueOf(position));
            Log.d("DEBUG", convertView.getTag().toString());

            return convertView;
        }
    }

ざっくりと検証用のコードを上みたいにかいてみたとして、動かすと以下の画面が出てくる。

f:id:satohu20xx:20120122214206p:plain

この画面が表示されるにはgetView()が11回呼ばれることになる。画面上に少しでもViewが出ていると判定されるとgetView()が呼ばれるみたい。んで、こいつをスクロールすると、12番目のオブジェクトが表示されることになる。1画面には11個のオブジェクトしか表示できる領域がないから12番目が表示されると0番目に表示されていたオブジェクトが見えなくなる。この見えなくなったーっていう判定をAndroidが勝手にやってくれるのがAdapterみたい。見えなくなったViewはメモリの無駄だから、この0番目のViewを使って12番目のViewを作るっていう仕組みがよく書いてある再利用という話。

Viewが再利用されると引数のconvertViewに見えなくなったView(今の例だと0番目のView)が設定されてgetViewが呼び出されることになる。convertViewがnullかどうかを判定してnullじゃなかったらわざわざnewすることなく使いまわしてやりましてやることでメモリが節約できて速度も上がって万々歳なんだとさ。

んで、よく画像を別のスレッドでダウンロードしてダウンロードし終わったらImageViewに設定してってことをやるんだけど、この再利用で0番目だったconvertViewがダウンロードしてる間にスクロールされて12番目のViewになってるとかがありえるわけさ。だからsetTagでなんらかの判別情報をいれといて再利用してるかどうかを判定してやることで別のイメージがダウンロードされても設定されないようにできるってことみたい。詳しい話は以下のURLを参考に

Android で ListView に非同期で取ってきた画像を表示したら位置がおかしい件 - slumbers

Android良く出来てる。