タグ「Android コード サンプル」が付けられているもの

 

TabActivity を利用するためのスケルトン。

package info.typea.kakeibot;

import android.app.TabActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TabHost;
import android.widget.TextView;

public class KakeibotActivity extends TabActivity  implements TabHost.TabContentFactory{
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final TabHost tabhost = getTabHost();
        
	// アクティビティ(CategoryPickupActivity) をタブにホストする
        tabhost.addTab(tabhost.newTabSpec("tab1")
                .setIndicator("categories", getResources().getDrawable(android.R.drawable.ic_menu_add))
                .setContent(new Intent(this, CategoryPickupActivity.class)
                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)));
        
        // アクティビティ(RegisterdItemPickupActivity)をタブにホストする
        tabhost.addTab(tabhost.newTabSpec("tab2")
                .setIndicator("items", getResources().getDrawable(android.R.drawable.ic_menu_delete))
                .setContent(new Intent(this, RegisterdItemPickupActivity.class)
                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)));

	// このアクティビティ自体をホストする(createTabContent が返す View が表示される)
        tabhost.addTab(tabhost.newTabSpec("tab3")
                .setIndicator("upload", getResources().getDrawable(android.R.drawable.ic_menu_upload))
                .setContent(this));
    }

    @Override
    public View createTabContent(String tag) {
        final TextView tv = new TextView(this);
        tv.setText("Content for tab with tag " + tag);
        return tv;
    }
}

tabactivity

カメラのプレビューが横向きに表示されてしまう!

X06HT Desre Android2.1 の時に、アプリでカメラを使用した Activity を作成していたのだが、普通に使うと何故か横向きにプレビューされてしまっていたので、何の気なしにか、なにかで調べてか忘れたが、以下の様なコードを書いて対応していた。

camera01

↑ こんな風にプレビューされるので、↓ こんな風に対応しておいたら、うまいこと動いていた。

Camera.Parameters p = camera.getParameters(); p.setRotation(90); camera.setParameters(p);

が、10月8日に、待ちわびた、Froyo化を行ったところ、元の横向きに戻ってしまい、Camera.Paramteres の値を変えても、書き方を変えても、うんともすんとも言わなくなってしまった。

不具合か~

と思ったが、どうも元々Androidのカメラとはそういうものらしい。

もはやケータイに必須のカメラをAndroidで制御しよう」によると、

そもそも、Android のカメラには、向きの概念が無くて、常にランドスケープモードで作動するようだ。

なんと。

なので、Activity のオリエンテーションモードを、ランドスケープにした上で、上記のようにRotation してあげれば、意図した方向に画面プレビューを表示出来ることが分かった。

・・・が、いくつか問題が。

メニューが横向きに出てきてしまう

camera02

こんな感じ。これは当然か~ でもこれじゃーメニューつかえねー

トーストが横向きに出てしまう。

これも使えない。ちなみに通知バーも横向きに出るので、フルスクリーンモードにして、通知バーも消す必要がある。

AndroidManifest.xml のアクティビティ要素にて、向きとフルスクリーン&タイトルバー無しを指定。
<activity android:name="info.typea.shujiroid.core.CameraActivity"
          android:screenOrientation="landscape"
          android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
あと、以下のパーミッションを指定する。
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-feature android:name="android.hardware.camera"></uses-feature>
<uses-feature android:name="android.hardware.camera.autofocus"></uses-feature>
<uses-feature android:name="android.hardware.camera.flash"></uses-feature>

カメラのコントロールのための UI を自分で作らなきゃいけない!?

まぁ、そもそも インテントで、カメラを呼び出せばいいだけかもしれないが(呼び出せるよね?)乗りかかった船なので、何とか使える形に持って行きたい。

先ほどのサイトのサンプルをダウンロードすると、AR(!?)のサンプルというか、プレビュー上にDroid君が表示される例があったので、参考にさせてもらい、Activity に カメラプレビュー用の SurfaceView を乗っけて、その上に、カメラコントロール用の View を乗っけることで対応してみた。

package info.typea.shujiroid.core;

import info.typea.shujiroid.free.R;
import info.typea.shujiroid.free.ShujiActivity;

import java.io.IOException;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.hardware.Camera;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Toast;

/**
 * カメラ用画面
 * 
 * @author piroto
 */
public class CameraActivity extends Activity {
    public static final String KEY_CAMERA_DATA = "camera_data";
    public static final String KEY_PREF_FLASH_MODE = "flash_mode";
    
    public static final int MENU_SHUTTER = Menu.FIRST;
    private SurfaceView preview;
    private SurfaceHolder holder;
    CameraControlView cameraCtrl;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_main);

        // カメラコントロール用ビュー
        cameraCtrl = new CameraControlView(this);
        addContentView(cameraCtrl, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
        
        preview = (SurfaceView) findViewById(R.id.surview_preview);
        
        holder = preview.getHolder();
        holder.addCallback(cameraCtrl);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
    
    /**
     * カメラのコントロール用のビュー
     * @author piroto
     */
    class CameraControlView extends View implements SurfaceHolder.Callback, 
                                                    Camera.ShutterCallback, 
                                                    Camera.AutoFocusCallback,
                                                    Camera.PictureCallback {
        SharedPreferences pref = null;
        private Camera camera;
        private boolean isInProcess;
        
        private float shutterX = 10f;
        private float shutterY = 10f;
        private float shutterR = 50f;

        private String currentFlashMode = null;
        private float flashX = 20f;
        private float flashY = 10f;
        private float flashIconSize = 100f;
        
        /**
         * @param context
         */
        public CameraControlView(Context context) {
            super(context);
            setFocusable(true);
            
            pref = PreferenceManager.getDefaultSharedPreferences(getContext());
            currentFlashMode = pref.getString(KEY_PREF_FLASH_MODE, Camera.Parameters.FLASH_MODE_OFF);
        }
        
        /**
         * フラッシュモードに応じたアイコンを取得する
         * @param flashMode
         * @return
         */
        private Bitmap getFlashModeIcon(String flashMode) {
            int iconId = R.drawable.flash_off_icon;
            if (Camera.Parameters.FLASH_MODE_AUTO.equals(flashMode)) {
                iconId = R.drawable.flash_auto_icon;
            } else if (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)) {
                iconId = R.drawable.flash_on_icon;
            } 
            return BitmapFactory.decodeResource(getResources(), iconId);
        }       
        
        /**
         * フラッシュモードを変更する
         * AUTO -> ON -> OFF
         */
        private void changeFlashMode() {
            if (Camera.Parameters.FLASH_MODE_AUTO.equals(currentFlashMode)) {
                currentFlashMode = Camera.Parameters.FLASH_MODE_ON;
            } else if (Camera.Parameters.FLASH_MODE_ON.equals(currentFlashMode)) {
                currentFlashMode = Camera.Parameters.FLASH_MODE_OFF;
            } else {
                currentFlashMode = Camera.Parameters.FLASH_MODE_AUTO;
            }
            
            // プリファレンスに設定を保存
            pref.edit().putString(KEY_PREF_FLASH_MODE, currentFlashMode).commit();

            Camera.Parameters p = camera.getParameters();
            p.setFlashMode(currentFlashMode);
            camera.setParameters(p);

            invalidate();
            return;
        }
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            
            int w = canvas.getWidth();
            int h = canvas.getHeight();
            
            // シャッターボタンを描画
            shutterX = w - (shutterR/2f+40f);
            shutterY = h / 2.0f;
            
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setStrokeWidth(4);
            paint.setColor(Color.BLUE);
            
            paint.setStyle(Style.FILL);
            paint.setAlpha(92);
            canvas.drawCircle(shutterX, shutterY, shutterR, paint);
            
            paint.setStyle(Style.STROKE);
            canvas.drawCircle(shutterX, shutterY, shutterR, paint);
            
            // フラッシュモードアイコンを描画
            canvas.drawBitmap(getFlashModeIcon(currentFlashMode), flashX, flashY, paint);
        }
        
        /**
         * 写真を撮る
         */
        public void takePicture() {
            isInProcess = true;
            camera.takePicture(this, null, this);
        }
        
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isInProcess) {
                    float x = event.getX();
                    float y = event.getY();
        
                    // シャッターが押されたかを判定
                    float minX = shutterX - shutterR;
                    float maxX = shutterX + shutterR;
                    float minY = shutterY - shutterR;
                    float maxY = shutterY + shutterR;
                    
                    if ( (minX <= x && x <= maxX) &&
                         (minY <= y && y <= maxY) ) {
                        takePicture();
                    }
                    
                    // フラッシュアイコンが押されたかを判定
                    minX = flashX;
                    maxX = flashX + flashIconSize;
                    minY = flashY;
                    maxY = flashY + flashIconSize;
                    if ( (minX <= x && x <= maxX) &&
                         (minY <= y && y <= maxY) ) {
                            changeFlashMode();
                    }
                }
                break;
            default:
                if (!isInProcess) {
                    camera.autoFocus(this);
                    isInProcess = true;
                }
                break;
            }
            return true;
        }
        
        @Override
        public void onShutter() {
        }

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            isInProcess = false;
        }

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            // 撮影した写真データを呼び出し元の Activity へ返す
            Intent intent = new Intent();
            intent.putExtra(KEY_CAMERA_DATA, data);
            setResult(ShujiActivity.REQUEST_CODE_CAMERA, intent);
            
            isInProcess = false;
            finish();
        }
        
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            try {
                camera = Camera.open();
                camera.setPreviewDisplay(holder);
            } catch (IOException e) {
                String msg = getString(R.string.msg_error_occured) + "\n"
                                 + e.getMessage();
                (Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG)).show();
            }
        }
        
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            camera.stopPreview();
            // 擬似的にポートレートモードに
            Camera.Parameters p = camera.getParameters();
            p.setRotation(90);
            p.setFlashMode(currentFlashMode);
            camera.setParameters(p);
            camera.startPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            camera.stopPreview();
            camera.release();
        }
    }
}

ちなみに、このアクティビティの呼び出しと、結果の受け取りは以下な感じ。

インテントを回答待ちで呼び出して、

// インテントを起動
startActivityForResult(new Intent(this, CameraActivity.class),
    REQUEST_CODE_CAMERA);

結果を byte 配列のまま受け取って、Bitmapに変換。

// 結果を受け取ってBitmapに変換
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    switch (requestCode) {
    case REQUEST_CODE_CAMERA:
        Bundle bundle = data.getExtras();
        byte[] picdata = bundle.getByteArray(CameraActivity.KEY_CAMERA_DATA);
        Bitmap pic = BitmapFactory.decodeByteArray(picdata, 0, picdata.length);
                :

拙いながらも、まぁこれでそれなりに動くようにはなった。

しかしながら、Home キー長押しで出てくるタスクリストは横向き。

だけど、標準のカメラも同じなので、割り切るしかないな。

 

あと、他機種で全く検証出来ないのが難点だが、知人がIS03を購入するようだし、これから Android もちが増えていくことを期待。

こんな感じになります。・・・わかりにくいかな。

 

また一歩野望に近づいた!

 

今のところ、自分はAndroid行き詰まったら、http://goosh.org/ で、how to use surfaceview? などと打ち込んで、stackoverflow の投稿に行き当たり、それで解決することが多いのですが、本書に一通り、ざっと目を通したところ、 HACKS の名の通り、「あの機能はどうやって実現するのだろう?」と気になったときに役に立ちそうなTIPSが満載となっています。

さっそく、「HACK #113 設定画面を簡単に作成する」 の力を借りる日がやってきました。

これをみてちゃっちゃと設定画面を実装しましょう。

android_preferences01

上記の様な画面を簡単に作成できるはず。

・・・ だったんですが、

android_preferences02

デフォルトで提供されている設定用のUIコンポーネントは、以下ぐらいらしく、上記例のような、SeekBar を利用する設定画面は、そこまで簡単には作成できないようで。。。できても良さそうなのに。。。

UI部品 内容
CheckBoxPreference チェックボックス
EditTextPreference テキストボックス
ListPreference ラジオボタンのリスト

RingtonePreference

着信音リスト

 

もう少し時間をかけて考える必要がありました。

結果、以下の様に、実装し、ほぼほぼ動いているので、手順をメモします。

基本的には、

  1. Preference クラスの作成
  2. Preference クラス用のレイアウトの作成
  3. PreferenceActivity 用 レイアウトの作成
  4. PreferenceActivity の作成

となります。

 

SeekBarPreference クラスの作成

まず、設定画面用のActivity である、PreferenceActivity にて使用する、UI部品である、Preference クラスを継承して、SeekBar を使った設定用部品を作成します。

レイアウトは、後で記述しますが、コンストラクタで XML(/res/layout/preference_widget_seekbar.xml) を読み込みます。

Log を出力したところ、以下の順でメソッドが呼ばれるようです。

  1. onGetDefaultValue  デフォルト値読み込み
  2. onSetInitialValue 初期値を設定
  3. onBindView ビューとデータをバインド
 package info.typea.shujiroid.core;
 
 import info.typea.shujiroid.free.R;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.preference.Preference;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
 
 /**
  * @author piroto
  */
 public class SeekBarPreference extends Preference implements OnSeekBarChangeListener {
 
     private static final int MAX_PROGRESS = 100;
     private static final int DEFAULT_PROGRESS = 50; 
     private int currentProgress;
     private int oldProgress;
     
     public SeekBarPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
         setWidgetLayoutResource(R.layout.preference_widget_seekbar);
     }
     
     /* 
      * Preference が 呼び出されるときにデフォルト値が読み込まれる必要がある
      * 異なる Preference 型は異なる 値型 は持つはずなので、サブクラスはそれにあわせた型を返す必要がある
      */
     @Override
     protected Object onGetDefaultValue(TypedArray a, int index) {
         return a.getInteger(index, DEFAULT_PROGRESS);
     }
 
     /* 
      * Preference の初期値を設定する
      * restorePersistedValue が true の場合、Preference 値を、SharedPreference からレストアすべき
      * false の場合 Preference 値にデフォルト値をセット
      * (SharedPreference の shouldPersist() が true の場合、可能ならSharedPreferenceに値を格納) 
      */
     @Override
     protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
         if (restorePersistedValue) {
             currentProgress = getPersistedInt(currentProgress);
         } else {
             currentProgress = (Integer) defaultValue;
             persistInt(currentProgress);
         }
         oldProgress = currentProgress;
     }
 
     /*
      * Preference のために、ビューとデータをバインドする
      * レイアウトからカスタムビューを参照しプロパティを設定するのに適する
      * スーパークラスの実装の呼び出しを確実に行うこと
      */
     @Override
     protected void onBindView(View view) {
         final SeekBar seekbar = (SeekBar) view.findViewById(R.id.pref_seekbar);
         if (seekbar != null) {
             seekbar.setProgress(currentProgress);
             seekbar.setMax(MAX_PROGRESS);
             seekbar.setOnSeekBarChangeListener(this);
         }
         super.onBindView(view);
     }
 
     @Override
     public void onStartTrackingTouch(SeekBar seekbar) {}
     @Override
     public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {}
     
     @Override
     public void onStopTrackingTouch(SeekBar seekbar) {
         int progress = seekbar.getProgress();
         /* ユーザーが設定変更を行った後(内部的な値を設定する前)に呼び出す。 */
         currentProgress = (callChangeListener(progress))?progress:oldProgress;
 
         persistInt(currentProgress);
         oldProgress = currentProgress;
     }
 }

SeekBarPreference UI 部品のレイアウトを定義(/res/layout/preference_widget_seekbar.xml)

今回 SeekBar のみで構成しましたが、通常の画面を作るように複数のコンポーネントを組み合わせることも出来そうです。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@+id/LinearLayout01" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >
    <SeekBar android:id="@+id/pref_seekbar" 
             android:layout_width="wrap_content" 
             android:layout_height="wrap_content"
             xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="100sp">
    </SeekBar>
</LinearLayout>

 

PreferenceActivity のレイアウト&定義(/res/xml/preferences.xml) を作成

設定画面のレイアウト XML を作成します。通常の画面レイアウトとは、置き場所や書き方が異なります。このあたりの詳細は、上記 Android Hacks ―プロが教えるテクニック & ツール を確認ください。

以下は、上記例の設定用XMLとなります。

android:key の値にて、SharedPreference から 設定値を取得することができる様になります。

問題の、SeekBarPreference の利用法は、info.typea.shujiroid.core.SeekBarPreference 要素を参照。

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
        xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
            android:title="@string/lbl_brush_pref_title">
        <info.typea.shujiroid.core.SeekBarPreference
                android:key="calligraphy_diameter"
                android:title="@string/lbl_diameter_of_brush"
                android:summary="@string/lbl_diameter_of_brush_summary"
                android:defaultValue="50"/>

        <info.typea.shujiroid.core.SeekBarPreference
                android:key="calligraphy_smoothness"
                android:title="@string/lbl_smoothness_of_brush"
                android:summary="@string/lbl_smoothness_of_brush_summary"
                android:defaultValue="50"/>

        <info.typea.shujiroid.core.SeekBarPreference
                android:key="calligraphy_alpha"
                android:title="@string/lbl_alpha_of_brush"
                android:summary="@string/lbl_alpha_of_brush_summary"
                android:defaultValue="50"/>

        <CheckBoxPreference
                android:key="calligraphy_antialias"
                android:title="@string/lbl_antialias"
                android:summary="@string/lbl_antialias_summary" />
        <ListPreference 
                android:key="calligraphy_color"
                android:title="@string/lbl_brush_color"
                android:summary="@string/lbl_brush_color_summary"
                android:entries="@array/ary_colors" 
                android:entryValues="@array/ary_color_values" />
        <ListPreference 
                android:key="calligraphy_background_color"
                android:title="@string/lbl_background_color"
                android:summary="@string/lbl_background_color_summary"
                android:entries="@array/ary_colors" 
                android:entryValues="@array/ary_color_values" />
        <CheckBoxPreference
                android:key="shujiview_vibrate"
                android:title="@string/lbl_vibrate"
                android:summary="@string/lbl_vibrate_summary" />
   </PreferenceCategory>
</PreferenceScreen>

設定画面(PreferenceActivity) の作成

res/xml/preferences.xml で定義した、Preference (設定項目のUIコンポーネント) から値を設定、取得するためのキーを、設定画面呼び出し元のActivity から参照できる様に定数化していますが、それ以外は何もしてません。PreferenceActivity を継承して自分のアプリケーション用の Activity を作成し、addPreferencesFromResource を利用して、設定ファイルを参照するだけです。

package info.typea.shujiroid.core;

import info.typea.shujiroid.free.R;
import android.os.Bundle;
import android.preference.PreferenceActivity;

public class ShujiPreferenceActivity extends PreferenceActivity {
    public static final String KEY_CALLIGRAPYHY_DIAMETER         = "calligraphy_diameter";
    public static final String KEY_CALLIGRAPYHY_SMOOTH           = "calligraphy_smoothness";
    public static final String KEY_CALLIGRAPYHY_ALPAH            = "calligraphy_alpha";
    public static final String KEY_CALLIGRAPYHY_ANTIALIAS        = "calligraphy_antialias";
    public static final String KEY_CALLIGRAPYHY_COLOR            = "calligraphy_color";
    public static final String KEY_CALLIGRAPYHY_BACKGROUNDCOLOR  = "calligraphy_background_color";
    public static final String KEY_SHUJIVIEW_VIBRATE             = "shujiview_vibrate";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

設定を利用する

これで、設定画面が完成しました。メインのアクティビティから利用してみます。

設定画面を呼び出す

任意の値(ここでは、REQUEST_CODE_PREFERENCS としました)を指定して、startActivityForResult を呼び出す。

private static final int REQUEST_CODE_PREFERENCES = 1;
   :
startActivityForResult(new Intent(this, ShujiPreferenceActivity.class),REQUEST_CODE_PREFERENCES);

設定画面を閉じる

設定画面を閉じると、onActivityResult が呼ばれるので、これをオーバーライドします。

先ほど設定した任意のコードが否かを requestCode で判定して、同一なら、SharedPrerferences から、設定値を取得します。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_PREFERENCES) {

        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
        
        int diameter = pref.getInt(ShujiPreferenceActivity.KEY_CALLIGRAPYHY_DIAMETER, 50);
        int smooth   = pref.getInt(ShujiPreferenceActivity.KEY_CALLIGRAPYHY_SMOOTH, 50);
                          :
    }
}

以上の手順で、カスタムPreference を利用した設定画面を作成することが出来ます。

最後の詰めを考えさせる作りになっているあたり、さすが ANDROID HACKS という気もします。

また一歩野望に近づいた!

AbstractHttpEntry を見ると、SDK Platform Android 2.1, API 7, revision 1  には、MIMEマルチパートを利用するためのライブラリは同梱されていないようだ。

このブログを参考(完全にそのまま)に、Android から、ファイルをMIMEマルチパートでアップロードしてみる。

まず、上述のように機能が同梱されていないので、以下のライブラリーを入手する必要があるそうだ。

今回は、以下の3つのアーカイブを入手

httpmime は、httpcomponents-client に httpclient とともにアーカイブされている。

で、それぞれ解凍して、以下の jar ファイルを Android アプリの libs フォルダに置いてあげる。

mime_android01

うーん、公開しているアプリケーション自体のファイルサイズが、118KB なのを考えると、結構サイズが大きいですね。サーバーサイドとは違って、気になりますね。

上記ブログでは言及されていない、httpcore-nio-4.0.1.jar はいるのかな?

はずして動くようなら、はずしちゃおう。

DefaultHttpClient client = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(uri);
File upfile = new File(info.getPath());

MultipartEntity entity = new MultipartEntity();
entity.addPart("upload_file_name",     new StringBody(upfile.getName()));
entity.addPart("upload_file_contents", new FileBody(upfile));
httpPost.setEntity(entity);
response = client.execute(httpPost);

あとは、これだけ。

無事、送信できることが確認できました。

また一歩野望に近づいた!

以下の記述で、動作したのでメモ。

HttpPost httpGet = new HttpGet(uri);
HttpParams httpParms = new BasicHttpParams();
httpParms.setIntParameter(AllClientPNames.CONNECTION_TIMEOUT, 3000);
httpParms.setIntParameter(AllClientPNames.SO_TIMEOUT, 5000);

DefaultHttpClient client = new DefaultHttpClient(httpParms);
HttpResponse response = client.execute(httpGet);

と、Android 端末のGoogle アカウントを用いて、GAEアプリケーションを利用する準備を徐々に進めてきていたのだが、GAEの開発環境でのやり方が分からなかったため、とりあえず実際にアプリケーションをリリースして試していた。

が、何とかいける方法が見つかったので、メモ。

通常PCから、GAEのアプリケーションをログインしないと使えないようにすると、以下の様なログイン画面にナビゲートされる。

gae_auth04 

ただ、Android 端末ではGoogle アカウントを管理しているので、アプリケーションがログインを必要とするGAEアプリケーションを利用する場合、以下の様な画面を表示させて、事前に認証を行っておくことが出来る。その手順は、前回実現した。

android_gae03

が、同じ手順で、ホスト名のみ、開発用のGAE の Webサーバーにしてもそのままでは動作しない。

まぁあくまで開発環境なので、認証周りまで完全にエミュレート出来ているわけでは無いのだろう。

開発用のサーバーでは、以下の様なログイン画面が表示され、何でもいいのでメールアドレスを入力してログインすると、目的のアプリケーションに遷移するようになっている。

gae_auth02

ということは、まぁ Cookie になにがしかの値を埋め込んでいるのだろうと言うことで、Firebug でのぞいてみると・・・

gae_auth01

こんなエントリーが。なんか、後半の数値の部分は動的に生成しているのかな?と思って、検索してみると数件そのままの値がヒット。

dev_appserver_login=test@example.com:False:185804764220139124118

なんか、固定値みたいなので、そのまま Cookie にセットして送信するコードを書いて、、、

String uri   = null;
String acsid = null;
HttpGet httpGet = null;
HttpResponse response = null;
if (Debug.isDebuggerConnected()) {
    // デバッグ時のコード
    uri   = "http://192.168.10.119:8080/cardroid";
    acsid = "dev_appserver_login=\"test@example.com:False:185804764220139124118\"";
} else {
    // リリース時のコード以下参照
    // http://typea.info/blg/glob/2010/08/android-google-gae.html 
}
HttpPost httpPost = new HttpPost(uri);
httpPost.setHeader("Cookie", acsid);

response = client.execute(httpPost);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    String l = null;
    while((l = reader.readLine()) != null) {
    Log.i("MyApp",l);
}

実行してみると、めでたく、ログイン状態となったようだ。

gae_auth03

これで、開発環境で、GAEアプリをテスト出来そうだ。

 

以上。

arrayadapter_with_radio

Android だと、リストビューから一件選択するのに、ラジオボタンが表示されることがよくある。

確かに、上記のようなダイアログを作ってて、対象が1件だけだと、ぱっと見メッセージと区別がつかないのでどうしていいか分からないが、ラジオボタンがあると「選択してね」感がかもしだされる。

ラジオボタンを使ってみる

で、ラジオボタンを配置しようと、単純にラジオボタンを含むレイアウト(R.id.rdo_select_item)を作成して動かしてみると・・・

ArrayAdapter<String> adapter 
    = new ArrayAdapter(context,
                               R.layout.row_items, 
                               R.id.rdo_select_item, 
                               items);

きちんと画面には表示されるが、リストビューの setOnItemClickListener に 登録したリスナーでリストの選択がどうもハンドリング出来ない。

android.R.layout.simple_spinner_dropdown_item を使ってみる

と、まぁ表示はされるのだが、文字色が黒だったり、どうもしっくりこない。

あと、選択したらきちんと動くのだが、ラジオボタンはフェイクのようで、選択状態の見た目にはならない。

ArrayAdapter adapter 
    = new ArrayAdapter(context,
                               android.R.layout.simple_spinner_dropdown_item, 
                               items);

TextView の代わりに CheckTextView を使いつつ、listChoiceIndicatorSingle を指定してみる

ArrayAdapter の使い方は、通常のTextView を単純に使用するのと同じにしておいて、

ArrayAdapter adapter 
    = new ArrayAdapter(context,
                               R.layout.row_items, 
                               R.id.txt_select_item, 
                               items);

レイアウトの方で、TextView の代わりに、CheckedTextView を使う。

そして、checkMark 属性に、android.R.attr.listChoiceIndicatorSingle を指定する。

<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" 
          android:id="@+id/txt_select_item" 
          android:layout_width="fill_parent" 
          android:layout_height="fill_parent" 
          android:textSize="20sp" 
          android:checkMark="?android:attr/listChoiceIndicatorSingle">
</CheckedTextView>

これで、見た目的には、希望通りになった。

ただ、android.R.layout.simple_spinner_dropdown_item 同様、ラジオボタンはフェイクだけど・・・ まぁいいや。

これに限らず、android.R には、こねたがいっぱいつまってそうなので、いずれ詳しくみてみたい。

また一歩野望に近づいた。

先日の勉強会で発表のために、Android アプリケーションの開発から公開までしたのだけれど、なにぶん知識と時間不足のため、ちょっと完成度低めだったので、少しずつ改良しながら、さらに野望の実現を目指す。

作成したのは、単語帳アプリケーションなのだが、単語帳のカードを途中までめくったところで端末の方向を切り替えると、最初のページに戻ってしまう。これはいただけない。修正する。

プライベートなプリファレンスをつかって、現在のページを保持させれば良いだろう。

getPreferences(MODE_PRIVATE) で、暗黙で Activity の クラス名をキーとしてgetSharedPreferencesを利用できる

プライベートなプリファレンスを使う例

 @Override
 protected void onPause() {
     Editor editor = getPreferences(MODE_PRIVATE).edit();
     // 現在ページを保存
      editor.putInt(PREF_KEY_CURRENT_PRACTICE, vfCard.getDisplayedChild());
     editor.commit();
     super.onPause();
 }
 @Override
 protected void onResume() {
     int idx = getPreferences(MODE_PRIVATE).getInt(PREF_KEY_CURRENT_PRACTICE, 0);
     // 現在ページを復帰
     if (0 <= idx && idx < vfCard.getChildCount()) {
         vfCard.setDisplayedChild(idx);
     }
     super.onResume();
 }

 

・・・ が、これがうまくいかない。

Activity のライフサイクルイベントのメソッドをオーバーライドして、ログを仕込んで、なにが起こっているか確認してみる

まずは、アプリケーションを起動し、対象の Activity を呼び出すと、

android_activity01 

09-02 23:32:43.878: INFO/MyApp(11021): onCreate()
09-02 23:32:44.668: INFO/MyApp(11021): onStart()
09-02 23:32:44.668: INFO/MyApp(11021): onResume()

onCreate から、onStart()、onResume() を経てActivity 実行中に。ふんふん。リファレンス通り。

 activity01

そして、画面を横にしてみる。

android_activity02

09-02 23:33:05.348: INFO/MyApp(11021): onPause()
09-02 23:33:05.348: INFO/MyApp(11021): onStop()
09-02 23:33:05.348: INFO/MyApp(11021): onDestroy()
09-02 23:33:05.428: INFO/MyApp(11021): onCreate()
09-02 23:33:06.358: INFO/MyApp(11021): onStart()
09-02 23:33:06.368: INFO/MyApp(11021): onResume()

activity02

おおっと、画面を横にした程度なら、onPause() から onResume() のコースを取ってくれても良さそうなものだが、一旦 Destroy までされてしまうようだ。

画面の縦と横では内容が全く異なるはずなので、画面自体の再構築の必要がある。当然と言えば当然か。

毎回 onCreate() も呼ばれてしまうとなると、onCreate() で初期化しようにも、Activity を新規に構築するのか、画面の向きが変わったのか判断つかない。 となると、Acitvity を呼び出すタイミングで、値をクリアする操作をしないといけない。 この場合、Activity プライベートなプリファレンスは使えないので、Activity 間で共有できるプリファレンスを使って書き換えてみる。

共有プリファレンスを使う例

 @Override
 protected void onPause() {
     Editor editor = getSharedPreferences(
                     PREF_KEY, 
                     MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE).edit();
     editor.putInt(PREF_KEY_CURRENT_PRACTICE, vfCard.getDisplayedChild());
     editor.commit();
     super.onPause();
 }

 @Override
 protected void onResume() {
     int idx = getSharedPreferences(PREF_KEY, MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE)
                   .getInt(PREF_KEY_CURRENT_PRACTICE, 0);
     if (0 <= idx && idx < vfCard.getChildCount()) {
         vfCard.setDisplayedChild(idx);
     }
     super.onResume();
 }

で、呼び出し元の Activity では、上記 onPause と同様にして、putInt() を利用して、 0 (初期値) をセットすれば、期待したように動くようになった。

よしよし。

 

・・・ が、ちょっとまった。プリファレンスは永続化される。 しかも自分が想定した利用法では、永続化する必要は全くない。たかだか Activity の一時的な状態を格納するだけに使うのはもったいない(?)のではないか。

 

Application の サブクラスを作成してそこに保持させる

ようなことを、ちらっとどこかで見たな~2chだったかな~

Activity は 破棄されても、Application は、基本生きているはず。ただ、システムによって、メモリが足りないときとかはKill されるということだったかと。リファレンス見てもそういう感じだ。

なので、Application が 不用意に Kill されても 保持していたいような設定などは、上記プリファレンスを使うことによって保持(永続化)し、まぁ使っている間だけ覚えていればいいや的な状態は、Application のメンバーとして持たせることにする。

Application のサブクラスを作成するのは、単純に、android.app.Application を継承したクラスを作成する。

ただし、それだけでは、そのアプリケーションクラスがメインのアプリケーションだと認識されていないので、AndroidManifest.xml に記述を追記してあげる必要がある。application の android:name に自作クラスを設定する。

<application android:name="CardroidApplication" 
                android:icon="@drawable/icon" 
                android:label="@string/app_name" 
            : 

もしくは、AndroidManifest.xml の設定画面、Application タブから、Name を設定する。

android_application

また一歩野望に近づいた。

 

How to use Multi-touch in Android 2 というサイトを参考に(というか基本的にそのまま)、ピンチイン・ピンチアウトを行うサンプルを気になった点をかいつまんでメモしながら実装してみる。

マルチタッチ

  • マルチタッチは通常のタッチスクリーンUIから、指を2本以上使えるようにしたシンプルな拡張
  • 画面上に2本の指を置き、指をつまむように狭めていくピンチインで表示が縮小したり、逆に離していくピンチアウトで表示が拡大させたりすることが出来る

簡単な画像ビューアを作る

  • スクリーン全体をカバーする大きなImageView を配置
  • JPEG か PNG フォーマットの画像(eg. at_hieizan.jpg 比叡山の写真です)を res/drawables-nodpi ディレクトリに置く
  • ImageView のソースに上記画像を指定、 android:src="@drawable/at_hieizan"
  • AndroidManifest.xml にて、@android:style/Theme.NoTitleBar.Fullscreen を指定(タイトルバーなし、フルスクリーン)

イベントをダンプしonTouch イベントで何が行われているかを確認する

  • dumpEvent() メソッドを作成し、onTouch() メソッドで何が行われているかを確認する
  • onTouch()  メソッド から true を返すことで、イベントをハンドリングしたことを明示する
  • event.getAction() の 下位8bitはアクションコード、次の8bitはポインターID なので、ビット演算 の & と ビットシフト >> それぞれに分離する。
  • event.getPointerCount() 何カ所ポイントされているか (X06HT では2カ所しか認識しないようだ)取得できる
  • event.getX(),event.getY() でそれぞれのポイントの座標が取得できる
  • getPointerId() で、それぞれのポイントのポインターIDについての情報かを判定出来る

変換行列

  • 画像の移動と拡大・縮小のために、ImageView クラスの 変換行列(matrix transformation)  を利用する
  • これにより、回転、傾斜付けなどいくつかの変換を行えるようになる
  • res/layout/main.xml で、android:scaleType="matrix"  とすることで利用可能になっている
  • 現在とオリジナルの2つの matrix をイメージを変換するのに使用する

ドラッグジェスチャーの実装

  • ドラッグジェスチャーは、最初の指がスクリーンを押して(ACTION_DOWN)開始され、離して(ACTION_UP もしくは ACTION_POINTER_UP)終了する
  • Android 組込のジェスチャーライブラリ は今回のケースでは使えない(マルチタッチをサポートしていない等)
  • 以下の例では、onTouch() メソッド中、ドラッグに関わる部分と ズームに関わる部分をわかりやすくするためだけに分けている。

ピンチジェスチャーの実装

感想

ページめくり同様、この手のUIはそこそこコードを書かないと実現出来ないのね。iPhone とかはどうなんだろう?簡単に実装できちゃうのかしら。

Youtube にあげた動画 のとおり、まぁ当たり前だが、サンプルのままだと挙動がかなり怪しいので、基本的な考え方を参考にしつつ、もう少し作り込まないと使えるものにはならなさそうではある。

アイディア的には、GestureWorks が参考になりそうな雰囲気をかなり醸し出している。自分のノートPCはタブレット & Windows7 なんだけど、マルチタッチ非対応なんでおそらくサンプル普通に実行できない(無理矢理する方法はありそう!?)んだよな~  残念。

 

Activity

 package info.typea.pinchzoom;
 
 import android.app.Activity;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.os.Bundle;
 import android.util.FloatMath;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
 import android.widget.ImageView;
 
 /**
  *
  */
 public class PinchZoomActivity extends Activity implements OnTouchListener {
     // 移動とズームに利用する
     // res/layout/main.xml にて android:scaleType="matrix" 指定  
     private Matrix matrix      = new Matrix();
     private Matrix savedMatrix = new Matrix();
     private PointF start       = new PointF();
     private float oldDist      = 0f;
     private PointF mid         = new PointF();
     private float curRatio     = 1f;
         
     // 以下の状態を取り得る
     private static final int NONE = 0;
     private static final int DRAG = 1;
     private static final int ZOOM = 2; 
     private int mode = NONE;
     
     /** Called when the activity is first created. */
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
         
         ImageView iv = (ImageView) findViewById(R.id.img_view);
         iv.setOnTouchListener(this);
     }
 
     @Override
     public boolean onTouch(View v, MotionEvent event) {
         ImageView view = (ImageView)v;
         
         // イベントのダンプ
         dumpEvent(event);
         
         /***********
          * ドラッグ 
          ***********/
         switch(event.getAction() & MotionEvent.ACTION_MASK) {
         case MotionEvent.ACTION_DOWN:
             savedMatrix.set(matrix);
             start.set(event.getX(), event.getY());
             Log.d("MyApp", "mode=DRAG");
             mode = DRAG;
             break;
         case MotionEvent.ACTION_UP:
         case MotionEvent.ACTION_POINTER_UP:
             mode = NONE;
             Log.d("MyApp", "mode=NONE");
             break;
         case MotionEvent.ACTION_MOVE:
             if (mode == DRAG) {
                 matrix.set(savedMatrix);
                 matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);
             }
             break;
         }
 
         /***********
          * ズーム
          ***********/
         switch(event.getAction() & MotionEvent.ACTION_MASK) {
         case MotionEvent.ACTION_POINTER_DOWN:
             oldDist = spacing(event);
             Log.d("MyApp", "oldDist=" + oldDist);
             // Android のポジション誤検知を無視
             if (oldDist > 10f) {
                 savedMatrix.set(matrix);
                 midPoint(mid, event);
                 mode = ZOOM;
                 Log.d("MyApp", "mode=ZOOM");
             }
             break;
         case MotionEvent.ACTION_MOVE:
             if (mode != DRAG) {
                 float newDist = spacing(event);
                 float scale = newDist / oldDist;
                 Log.d("MyApp", "scale=" + scale);
                 float tmpRatio = curRatio * scale;
                 if (0.1f < tmpRatio && tmpRatio < 20f) {
                     curRatio = tmpRatio;
                     matrix.postScale(scale, scale, mid.x, mid.y);
                 }
             }
             break;
         }
         
         // 変換の実行
         view.setImageMatrix(matrix);
         
         return true; // イベントがハンドリングされたことを示す
     }
     /**
      * 2点間の距離を計算
      */
     private float spacing(MotionEvent event) {
         float x = event.getX(0) - event.getX(1);
         float y = event.getY(0) - event.getY(1);
         return FloatMath.sqrt(x * x + y * y);
     }
     /**
      * 2点間の中間点を計算
      */
     private void midPoint(PointF point, MotionEvent event) {
         float x = event.getX(0) + event.getX(1);
         float y = event.getY(0) + event.getY(1);
         point.set(x / 2, y / 2);
     }
     private void dumpEvent(MotionEvent event) {
         String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
                              "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
         StringBuilder sb = new StringBuilder();
         int action = event.getAction();
         // event.getAction() の 下位8bitはアクションコード、次の8bitはポインターID 
         // ビット演算 の & と ビットシフト>> で分離する。
         int actionCode = action & MotionEvent.ACTION_MASK;
         sb.append("event ACTION_" ).append(names[actionCode]);
         if (actionCode == MotionEvent.ACTION_POINTER_DOWN
          || actionCode == MotionEvent.ACTION_POINTER_UP) {
             sb.append("(pid " ).append(
                     action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
             sb.append(")" );
         }
         sb.append("[" );
         // event.getPointerCount() 何カ所ポイントされているか、
         // event.getX(),event.getY() で座標が取得できる
         // getPointerId() で、どのポインターIDについての情報かを判定出来る
         for (int i = 0; i < event.getPointerCount(); i++) {
             sb.append("#" ).append(i);
             sb.append("(pid " ).append(event.getPointerId(i));
             sb.append(")=" ).append((int) event.getX(i));
             sb.append("," ).append((int) event.getY(i));
             if (i + 1 < event.getPointerCount())
                 sb.append(";" );
         }
         sb.append("]" );
         Log.d("MyApp", sb.toString());
     }
 }

Layout

 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     >
     <ImageView android:layout_width="wrap_content" 
                android:layout_height="wrap_content" 
                android:id="@+id/img_view" 
                android:scaleType="matrix" 
                android:src="@drawable/at_hieizan">
     </ImageView>
 </LinearLayout>

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="info.typea.pinchzoom"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".PinchZoomActivity"
                  android:label="@string/app_name"
                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest> 

いじょ。

Android アプリから、GAE の認証を行うことは出来たのだが、GAE の SDKに含まれる開発環境にうまく接続することが出来ない。

仕方がないので、デバッグ中か否かを判定して、デバッグ中の場合は、接続先のURLをローカルアドレスに変更し、認証プロセスをスキップさせようと思いそれらしいAPIを探してみる。

android.os.Debug.isDebuggerConnected()

あった。

・・・ が、試せど試せど 上記の結果が true にならない。いらいらいらいら。

しらべてたら、SDKにこんな記述が。

Declare your application as debuggable in your manifest

マニフェストファイルに書けばいいのね。ふーん。

で、見てみると、ありました、Debuggable = true に設定。

android_debugable01

LogCat に出力するとキャプチャがとれないし、false の場合確認が出来ないので、Toast に表示させるべく、メイン Activity の onCreate に以下を記述。

(Toast.makeText(this, 
    String.valueOf("Debug.isDebuggerConnected() = " + Debug.isDebuggerConnected()), 
    Toast.LENGTH_LONG)).show();

(が、Toast にしてもキャプチャはとれないことに後で気づく)

起動してみると、以下の様なダイアログが。ちゃんと機能してるっぽい。あたりまえか。

 android_debugable02

そして、きちんと Toast に true が表示されました。

もちろん USB デバッグを停止して実行すると false が返りました。これでデバッグ時と通常時で挙動が切り替えられます。一歩野望に近づきました。

android_debugable03

Eclipse のプラグインからもいろいろ操作ができるようになりました。

android_debugable04   

リリースするときには false に戻した方が良さそうです。

Android と GAE を同時に Ecliipse からデバッグする環境は出来たので、Google アカウントを Android アプリケーションから利用してみる。

GAE アプリケーション側でログイン状態を判定

android_gae_07

typea_android_apps.py

/cardroid というリクエストが来たら、cardroid パッケージ cardroid_request_handler モジュールの InitialCardroidPage クラスを呼び出すように設定

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

from cardroid import cardroid_request_handler as cardroid_handler

class MainPage(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('Hello, webapp World!')

application = webapp.WSGIApplication([
                                      ('/', MainPage), 
                                      ('/cardroid', cardroid_handler.InitialCardroidPage),
                                     ], debug=True)
def main():
    run_wsgi_app(application)

if __name__ == "__main__":
    main()

cardroid_request_handler.py

ユーザーがログイン状態であれば、ユーザーのニックネームとログアウトのリンク を返し、そうでなければ、ログインページへのリンクを返すようにする。

from google.appengine.ext import webapp
from google.appengine.api import users

class InitialCardroidPage(webapp.RequestHandler):
    def get(self):
        self.post()
        
    def post(self):
        user = users.get_current_user()
        if user:
            user_logout_url = users.create_logout_url("/")
            return self.response.out.write("<html>%s <a href="%s">logout</a>" % (user.nickname(), user_logout_url));
        else:
            user_login_url  = users.create_login_url('/cardroid')
            return self.response.out.write("<html><a href="%s">login</a>" % (user_login_url));

PCのブラウザから確認

未ログイン状態の場合、ログインのリンクを表示

android_gae_08

ログインする

android_gae_09

ユーザーのニックネームとログアウトのリンクを表示

 android_gae_10

Android アプリケーションから Google アカウントを利用して GAE アプリケーションを呼び出す。

このあたりを参考に。


GaeUtil クラス

とりあえず、GAE 関係で利用するクラスをまとめるために作成。Static クラスの便利さに遅ればせながら気がついたので、使いまくり。

特に意味は無い。

PracticeUploader

名前は、自分が作っているアプリにちなんでいるだけでこちらも意味は無い。upload() メソッドの中からが本筋。

AccountManager

getAccounts() メソッドを利用すると、Android 端末で管理しているアカウント情報が取得できるようだ。

android_gae06

これらね。

で、それぞれのアカウントは type 情報を持っていて、getAccountsByType("com.google")  で、Google アカウントの情報がとれてくる。

とりあえず、添え字=0 の値を利用しているが、マルチアカウントだと複数件とれてくるのかな?

その場合、選択させる仕組みも必要かも。

で、getAuthToken() メソッドを利用して、認証用のトークンを取得。

取得したアカウントオブジェクトと、"ah" と コールバック用のインスタンスを渡す。

Google アカウントは "ah"  のようだ。認証用のトークンが取得できたら、渡したインスタンスがコールバックされる。

UploadPracticeCallback

このクラスも名前は意味がない(というか自分向け)、上記で参照したコード等は、非同期で処理を行ったりしているが、まずは挙動確認のため、べた書きする。

どうやら、認証が初回等必要な場合は、Intent intent = (Intent)bundle.get(AccountManager.KEY_INTENT);  で Intent が取得できるようなので、startActivity してあげると、認証を確認する画面が表示される。

android_gae03

認証されていれば、String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); で、認証トークンが取得できる。

Cookie の保持

  認証トークンを含めて、GAE アプリケーションへ 以下の感じでGET リクエストを投げる。

https://{ GAEアプリケーションURL }/_ah/login?continue={ 認証後に遷移するURL }&auth={ 認証トークン }

http か、https かによって、Cookie に "ACSID" または、"SACSID" のキーが設定されてくるので、保持しておく。

 

実際のリクエスト

これで、実際にやりたいリクエストを投げる準備が整った。

上記で保持した Cookie を付加して、POST メソッドを投げる。

ログイン状態になっていれば、ニックネームが正しく LogCat にはかれるはず。

 

問題点

以下、GAE アプリケーションを、リリースして確認した。開発環境でデバッグしたい のだが、現時点ではどうしていいのかわからん。

 ==> 2010/09/08 解決(?) しました!

android_gae_11

お、来ました。とりあえず、成功!!

開発環境でどうやるかは、今後の課題。

デバッグモードとリリースモードでコードの切り替えが必要になったりするのかなぁ。

package info.typea.cardroid;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

public class GaeUtil {
    /**
     * 
     */
    public static class PracticeUploader {
        private Context context;
        public PracticeUploader(Context context) {
            this.context = context;
        }
        public void upload() {
            AccountManager accountManager = AccountManager.get(context);
            Account[] accounts = accountManager.getAccountsByType("com.google");
            
            if (accounts.length >= 0) {
                Account account = accounts[0];
                accountManager.getAuthToken(account, 
                                            "ah", 
                                            false, 
                                            new UploadPracticeCallback(this.context), 
                                            null);
            }
        }
    }
    
    /**
     *
     */
    public static class UploadPracticeCallback implements AccountManagerCallback {
        private final String GAE_APP_URI = "http://typea-android-apps.appspot.com";
        
        private Context context;
        public UploadPracticeCallback(Context context) {
            this.context = context;
        }
        @Override
        public void run(AccountManagerFuture result) {
            Bundle bundle;
            try {
                bundle = result.getResult();
                Intent intent = (Intent)bundle.get(AccountManager.KEY_INTENT);
                if (intent != null) {
                    this.context.startActivity(intent);
                } else {
                    String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);

                    DefaultHttpClient client = new DefaultHttpClient();
                    client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);

                    String uri = GAE_APP_URI + "/_ah/login?continue=/cardroid&auth=" + authToken;
                    
                    HttpGet httpGet = new HttpGet(uri);
                    HttpResponse response = client.execute(httpGet);
                    String acsid = null;
                    for (Cookie cookie : client.getCookieStore().getCookies()) {
                        if ("SACSID".equals(cookie.getName()) ||
                                "ACSID".equals(cookie.getName())) {
                            acsid = cookie.getName() + "=" + cookie.getValue();
                        }
                    }
                    
                    uri = GAE_APP_URI + "/cardroid";
                    
                    HttpPost httpPost = new HttpPost(uri);
                    httpPost.setHeader("Cookie", acsid);
                    
                    response = client.execute(httpPost);
                    InputStream in = response.getEntity().getContent();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        String l = null;
                        while((l = reader.readLine()) != null) {
                            Log.i("MyApp",l);
                    }
                }
            } catch (OperationCanceledException e) {
                showMessage(R.string.lbl_operation_canceled);
            } catch (AuthenticatorException e) {
                showMessage(R.string.lbl_authenticator_failed);
            } catch (IOException e) {
                showMessage(R.string.lbl_error);
            }
        }
        /**
         * @param resId
         */
        private void showMessage(int resId) {
            (Toast.makeText(this.context,resId, Toast.LENGTH_LONG)).show();
        }
    }
}
  

以上。

最近、無料アプリケーションのアップデートを Android マーケットで行うと、もれなく広告がついてくるようになりました。

画面が狭くなって使う側としてはあまりうれしくはありませんが、おそらく今後(今も?)アプリケーションを販売することは困難になると思われるので、無料版に広告がつくのが自然の流れになった方が、開発していこうとする側としては望ましいかなと思います。

で、広告をアプリケーションに表示すべく、AdMob のアカウントを登録してみようと思う。

AdMob サイトにアクセス

http://jp.admob.com/

右上の 登録 リンクをクリック

admob01 

アカウント情報を登録

admob02

登録を行うと、確認メールが送信される。

送信されためーるのURLをクリック。

admob03

アカウントが有効化された!

右の「最初のサイトを追加する」をクリック

admob04

アプリケーションタイプの選択

Androidアプリケーションを選択する。

admob05

アプリケーション情報の登録

まだ、公開していないアプリケーションでも、下例のとおり、パッケージ名を指定すれば登録出来る。

market://details?id=<packagename>

admob12

SDKのダウンロード

AdMob Android SDK をダウンロードして、アプリケーションに組み込む必要がある。

手順は、すぐしたにリンクされている PDF にまとめられているが、若干手数がかかる。

こちらにSDKの利用法を簡単にメモ

admob06

広告の収益情報を確認できる様になった

admob07

支払情報の設定

支払を受けるためには、連絡先情報を登録する必要がある

admob08 

admob09 

パブリッシャーID の取得

パブリッシャーID を アプリケーションに組み込む必要があるが、上記で追加したアプリケーションの「設定を管理」をクリックすると、アプリケーション画面にパブリッシャーIDが表示されている。

admob10 

admob11

以上で、AdMob への登録は完了。

アプリケーションへの組込については、こちら にまとめ。

今日は、X06HTが販売終了するって話が飛び交っている。

有機ELの供給不足でX06HT自体を安定供給できないための一時販売休止だと思いたい。公式発表を待つ。

販売終了でも、Softbank から、Android 携帯が発売されれば、まぁ良とするが、されなかったら至極残念だ。

 

まぁいいや。

Android では、ダイアログボックスを使うのに、JavaScript の alert や confirm みたいに気軽に使えるといいのだけど、どうも用意されていないみたい。使い回したいので共通化しときたいが、どうしたらいいかな~と。

手元の本 にも、そこまでのことは書いてない。

 

一晩考えて、使えそうな形になったので、メモ

共通で作成するダイアログの種類

  • alert ・・・ OK ボタンのみ。警告
  • confirm ・・・ OK 、Cancel 確認
  • inputBox ・・・ 入力エリア、OK、Cancel

の3つをアプリケーションのどこからでも使えるように、android.app.Application クラスを継承して、static メソッドとしてそれぞれ作成する。

問題点

alert は、単に OK ボタンがおされたら閉じるだけなので、特に問題ない。

問題は、残りの2つダイアログでのユーザー入力をどうやって呼び出し元に返すか。

 

JavaScript 等のように結果を Boolean や String で単純に受け取るわけには、いかなそうだ。

EventLister( OnClickListener ) を利用方法に合わせて個別に作成する方法をとってみる。もっと良い方法があれば知りたいが。

対応

confirm については、AlertDialog.Builder を利用するため、ClickListener にダイアログの参照が渡ってくるので、これをつかってダイアログを消すことができる。

inputBox については、UI を XML で定義することにしたので、View.OnClickListener でイベントを取る必要があるが、これだと、ボタン押下時にダイアログを消すことができない。

苦肉の策として、View.OnClickListener を 実装した static 抽象クラス InputBoxOnClickListener  を アプリケーションクラスにもたせ、inputBox メソッドの中でダイアログを生成した後、参照をセットするようにしてみた。

 

 package info.typea.myapp;
 
 import android.app.AlertDialog;
 import android.app.Application;
 import android.app.Dialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.view.View;
 import android.widget.Button;
 import android.widget.EditText;
 
 public class MyApplication extends Application {
 
     /**
      * Application 共通警告ダイアログ
      * @param titleId
      * @param msg
      */
     public static void alert(Context context, int titleId, String msg) {
         AlertDialog.Builder b = new AlertDialog.Builder(context);
         b.setTitle(titleId);
         b.setIcon(android.R.drawable.ic_dialog_alert);
         b.setMessage(msg);
         b.setPositiveButton("OK", new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
                 dialog.dismiss();
             }
         });
         b.show();
     }
 
     /**
      * Application 共通 確認ダイアログ
      * @param titleId
      * @param msgId
      */
     public static void confirm(Context context, int titleId, int msgId, 
                                 DialogInterface.OnClickListener lisner) {
         AlertDialog.Builder b = new AlertDialog.Builder(context);
         b.setTitle(titleId);
         b.setIcon(android.R.drawable.ic_dialog_info);
         b.setMessage(msgId);
         b.setPositiveButton("OK", lisner);
         b.setNegativeButton("Cancel", lisner);
         b.show();
     }
     
     /**
      * Application 共通 入力ダイアログ
      * @param context
      * @param msg
      * @param lsnr
      */
     public static void inputBox(Context context, String title, 
                                 String contents, InputBoxOnClickListener lsnr ) {
         
         Dialog dialog = new Dialog(context);
         dialog.setContentView(R.layout.input_dialog);
         dialog.setTitle(title);
         
         EditText txtContent = (EditText) dialog.findViewById(R.id.txt_contents);
         txtContent.setText(contents);
         
         Button btnOk     = (Button) dialog.findViewById(R.id.btn_ok);
         Button btnCancel = (Button) dialog.findViewById(R.id.btn_cancel);
         
         lsnr.setDialog(dialog);
         
         btnOk.setOnClickListener(lsnr);
         btnCancel.setOnClickListener(lsnr);
         
         dialog.show();        
     }
     
     /**
      * Application 共通入力ダイアログ 用 Click イベントリスナー
      */
     public static abstract class InputBoxOnClickListener implements View.OnClickListener {
         // ダイアログを消すためにダイアログの参照を保持できるように
         protected Dialog dialog;
         public void setDialog(Dialog dialog) {
             this.dialog = dialog;
         }
         public void dismiss() {
             if (this.dialog != null) {
                 this.dialog.dismiss();
             }
         }
     }
 }

inputBox の XML レイアウト

 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:gravity="center">
    <EditText android:text="" android:id="@+id/txt_contents" 
                              android:layout_width="wrap_content" 
                              android:layout_height="wrap_content" 
                              android:minWidth="200sp" 
                              android:scrollbars="vertical" 
                              android:maxLines="3">
    </EditText>
    <LinearLayout 
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <ImageView android:id="@+id/img_icon" 
                   android:layout_width="wrap_content" 
                   android:layout_height="wrap_content">
        </ImageView>
        <TextView android:text="" android:id="@+id/txt_msg" 
                 android:layout_width="wrap_content" 
                 android:layout_height="wrap_content">
        </TextView>
    </LinearLayout>
    <LinearLayout 
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" android:gravity="center">
        <Button android:text="@string/lbl_btn_ok" 
                android:id="@+id/btn_ok" 
                android:layout_height="wrap_content" 
                android:minWidth="80sp" 
                android:layout_width="wrap_content">
        </Button>   
        <Button android:text="@string/lbl_btn_cancel" 
                android:id="@+id/btn_cancel" 
                android:layout_height="wrap_content" 
                android:minWidth="80sp" 
                android:layout_width="wrap_content">
        </Button>
    </LinearLayout>
 </LinearLayout>

呼び出し方

ここまで、準備すれば、あとは好きなように Listener を 実装して、Click イベントをハンドリングすればいい。

例えば、confirm を利用して、ファイルの削除確認をしたいなら以下の様なファイル削除用のリスナーを作成して、

 class FileDeleteConfirmDialogOnClickListener implements DialogInterface.OnClickListener {
    private HogeInfo hogeInfo;  // 何かしらダイアログで操作したい情報をリスナーに詰めておく
    public FileDeleteConfirmDialogOnClickListener(HogeInfo hogeInfo) {
        super();
        this.hogeInfo = hogeInfo; 
    }
    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (DialogInterface.BUTTON1 == which) {
            // OK が押されたらファイルを削除する処理をさせる
            File f = new File(hogeInfo.getPath());
            if (f.isFile()) {
                f.delete();
            }
        } else if 
            (DialogInterface.BUTTON2 == which) {
            // NOP
        }
        dialog.dismiss();
    }
 }

こんな、感じで呼び出せばいい。

 MyApplication.confirm(this, R.string.lbl_confirm_dialog_title, R.string.confirm_delete_file, 
     new FileDeleteConfirmDialogOnClickListener(hogeInfo));

inputBox を利用して、新規で作るファイル名を指定させたいなら、

 class FileNameDialogOnClickListener extends MyApplication.InputBoxOnClickListener {
     @Override
     public void onClick(View v) {
         if (v.getId() == R.id.btn_ok) {
         
             EditText txtContent = (EditText) dialog.findViewById(R.id.txt_contents);
             // 入力された値はファイル名
             String filename = txtContent.getText().toString();
             try {
                 File f = new File(filename);
                 f.createNewFile();
             } catch(Exception e) {
                 ((ImageView) dialog.findViewById(R.id.img_icon))
                     .setImageResource(android.R.drawable.ic_dialog_alert);
                 ((TextView) dialog.findViewById(R.id.txt_msg))
                     .setText(e.toString());
                 return;
             }
         }
         this.dialog.dismiss();
     }
 }

こんな感じの専用リスナーを作成して、

 Date d = new Date();
 String msg = String.format("card_%tY%tm%td%tH%tM",d,d,d,d,d);
 String title = "filenam?";
 MyApplication.inputBox(this, title, msg, new FileNameDialogOnClickListener());

こんな感じで呼び出せばいい。

device01 device02

いじょ。

あの魅力的な iPad の本のページをめくる様なアニメーション。

そこまでとは行かないまでも、ちょっとしたページをめくるような動きは、ちょっとした設定でできるんだろうAndroid。位に思っていたが、そうでもない感じ。

どうするのが定石なのかちと分からないが、それっぽく動くようになったので、メモしておく。

ViewFlipper とアニメーションを組み合わせる

ViewFlipperっていうくらいだから、こいつを使えば万事OKと思っていた。

確かに、簡単にできる。

以下のソースは、3つの実装を切り分けるような形式になっているので無駄に長いが、この実装をおこなっているのは、pageFlipWithSimpleAnimation() メソッド。

ページ遷移は、ViewFlipperに任せて、動きはアニメーションで行う。

アニメーションサンプルの読み込みと作成

アニメーションって急に言われても取っつきにくいが、サンプルがSDKにある。 自分の環境だと、以下

{android-sdk}\samples\android-7\ApiDemos\res\anim

適宜読み替えて。

Eclipse から、/res/anim フォルダをつくって、コンテキストメニューから上記フォルダのアニメーションファイルを適宜インポート。

android_anim01

ここでは、push_left_in.xml、push_left_out.xml をそのまま、使う。

これは、View を 左に向けて 入ってくるのと、左に向けて出て行く アニメーション。

逆に右に向けて入ってくるのと、右に向けて出ていくのが必要になるので、上記をコピーして、push_right_in.xml、push_right_out.xml を作成。

<translate android:fromXDelta="100%p" 
              android:toXDelta="0" 
              android:duration="300"/>
<alpha     android:fromAlpha="1.0"
              android:toAlpha="1.0"
              android:duration="300" />

中身を見るとこんな記述がされている。

見慣れないのでとまどうが、要するに、translate(移動する)、X軸上で、fromXDelta (どこから) toXDelta(どこまで) を duration(どれだけの時間をかけて)と、alpah (透明度) を fromAlpa(1.0 なら不透明) から toAlpha(0.0 なら透明)  という設定だけなので、基本 left の 反対を設定するように right の設定ファイルを作成する。

基本以上。あとは、動きに合わせて、前ページか次ページか判定し、それに合わせたアニメーションでページ遷移する。

ごりごりページめくりを実装する

で、ViewFlipperを使えば、ページめくりを途中で止めたりとかできるかと思いきやそうでもないようだ。(できるのかな?)

なので、ごりごり実装してみる。

基本、同じレイアウトファイルのViewFlipper 配下に、ページ設定のレイアウトを配置するようだが、外に出してみる。

LayoutInflater.inflate を利用すると、動的にレイアウトを読み込むことができる。

あと、はまったのが、ViewFlipper の addView メソッドでインデックスを引数にとるが、インデックスとビューとの対応が、なぜか動かすと変わってしまって、たとえば、インデックス=1 で add したつもりの ビューが 2  getChildAt(2) で取れてきたり、setDisplayedChild(1) で、インデックス=0 で設定したつもりのビューがアクティブになってしまったり。。。そもそも使い方が悪いのか知らん。

なので、動的にViewの内容を入れ替えることを想定して、前、今、後 の3つのレイアウトを用意し、それぞれのID を int[] viewOrder に外だしして管理するようにした。

あとは、View.layout で、それぞれのビューの表示、非表示を切り替えながら、位置を変えていくことで、それなりに動くようにはなった。

課題

一応、

  1. ViewFlipper + アニメーションをつかった単純なページめくり
  2. 今のページをずらして、次や前のページがあわられる
  3. 次や前のページがあらわれて、今のページを隠す

3パターンを作ってみた。layout の位置の設定だけで、Desire の ホーム画面のような連続して切り替わるような形にもできそうだ。

3の「次や前のページがあらわれて、今のページを隠す」 場合、一瞬画面がちらつく。これは何とかならんかな。

 

package info.typea.viewflipperapp;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.ViewFlipper;

public class ViewFilpperAppActivity extends Activity implements OnTouchListener {
    private static final String LOG_TAG = "MyApp";
    
    /* コンテキストメニューID */
    private static final int MENU_VIEWFLIP_ANIM  = Menu.FIRST; 
    private static final int MENU_MOVE_NEXT      = Menu.FIRST + 1; 
    private static final int MENU_MOVE_CURRENT   = Menu.FIRST + 2; 
    
    /* ページ切り替え動作モード */
    private int procMode = MENU_VIEWFLIP_ANIM;
    
    /* ページを切り替える移動量閾値 */
    private final int SWITCH_THRESHOLD = 10;
    
    /* ページ切り替えモード */
    private final int FLIPMODE_NOMOVE = 0;
    private final int FLIPMODE_NEXT =  1;
    private final int FLIPMODE_PREV = -1;
    private int flipMode = FLIPMODE_NOMOVE;
         
    private ViewFlipper vf    = null;
    private View currentView  = null;
    private View nextView     = null;
    private View prevView     = null;
    
    /* ページのIDと順序を管理 */
    private int viewOrder[]   = null;
    private int curIdx = -1;
    private int preIdx = -1;
    private int nxtIdx = -1;
    
    private int movePageThreshold = 0;
    private float startX;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        LinearLayout layout = (LinearLayout) findViewById(R.id.layout_main);
        layout.setOnTouchListener(this);
    
        vf = (ViewFlipper) findViewById(R.id.details);

        /* 
         * レイアウトを実行時にインスタンス化し ViewFlipperに格納 
         * 同時に順序をIDで管理する
         * addView(v,idx) で管理できると思いきや、実行時に View と Index の対応が変わってしまうようだ
         */
        LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        int layouts[] = new int[] {R.layout.layout_page1,
                                    R.layout.layout_page2,
                                    R.layout.layout_page3};
        viewOrder = new int[layouts.length];
        for (int i=0; i<layouts.length; i++) {
            View vw = vi.inflate(layouts[i], null);
            // ViewFlipper に格納
            vf.addView(vw);
            // ID管理用配列に保持
            viewOrder[i] = vw.getId();
        }
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // コンテキストメニューで、ページめくりの実装を切り替える
        super.onCreateOptionsMenu(menu);
        menu.add(0, MENU_VIEWFLIP_ANIM, 0, R.string.vewflipper_anim);
        menu.add(0, MENU_MOVE_CURRENT,  0, R.string.current_page_move);
        menu.add(0, MENU_MOVE_NEXT,     0, R.string.next_prev_page_move);
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        procMode = item.getItemId();
        
        // ViewFlipper に設定したアニメーションを解除
        vf.setInAnimation(null);
        vf.setOutAnimation(null);
        
        return true;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        boolean ret = false;
        
        // ページめくりの実装を切り替える
        switch(procMode) {
        case MENU_VIEWFLIP_ANIM:
            ret =  pageFlipWithSimpleAnimation(v, event);
            break;
        case MENU_MOVE_CURRENT:
            ret = pageFlipWithFingerMoveCurrent(v, event);
            break;
        case MENU_MOVE_NEXT:
            ret = pageFlipWithFingerMoveNext(v, event);
            break;
        }
        return ret;
    }

    /**
     * ViewFlipper と アニメーションを利用して単純にページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithSimpleAnimation(View v, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            float currentX = event.getX();
            if (this.startX > currentX ) {
                vf.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_left_in));
                vf.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_left_out));
                vf.showNext();
            }
            if (this.startX  < currentX) {
                vf.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_right_out));
                vf.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_right_in));
                vf.showPrevious();
            }
        default:
            break;
        }
        return true;
    }

    /**
     * 前のページおよび次のページを移動することによってページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithFingerMoveNext(View v, MotionEvent event) {
        float currentX = event.getX();
        
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();

            currentView = vf.getCurrentView();
            movePageThreshold = (currentView.getWidth() / 5);
            
            int viewCount = viewOrder.length;
            for (int i=0; i<viewCount; i++) {
                //Log.i("MyApp", "ord=id:" + viewOrder[i] + "==" + cv.getId());
                if (viewOrder[i] == currentView.getId()) {
                    curIdx = i;
                    break;
                }
            }
            
            if (curIdx >= 0) {
                preIdx = curIdx - 1;
                nxtIdx = curIdx + 1;
                preIdx = (preIdx < 0         )?viewCount - 1:preIdx;
                nxtIdx = (nxtIdx >= viewCount)?0            :nxtIdx;
                
                prevView = vf.findViewById(viewOrder[preIdx]);
                nextView = vf.findViewById(viewOrder[nxtIdx]);
            }
            
            Log.i(LOG_TAG, 
                    String.format("Pre=%d(%d),Cur=%d(%d),Nxt=%d(%d)" 
                                    ,preIdx,prevView.getId()
                                    ,curIdx,currentView.getId()
                                    ,nxtIdx,nextView.getId()));
            
            break;
        case MotionEvent.ACTION_MOVE:
            int travelDistanceX = (int)(currentX - this.startX);
            int fingerPosX = (int)currentX;
            
            if (flipMode == FLIPMODE_NOMOVE) {
                if (travelDistanceX > SWITCH_THRESHOLD) {
                    flipMode = FLIPMODE_PREV;
                } else if
                   ( travelDistanceX < (SWITCH_THRESHOLD * -1)  ) {
                    flipMode = FLIPMODE_NEXT;
                } else {
                    flipMode = FLIPMODE_NOMOVE;
                }
            }
            
            if (flipMode == FLIPMODE_PREV) {
                prevView.layout(fingerPosX - prevView.getWidth(), 
                          prevView.getTop(), 
                          fingerPosX , 
                          prevView.getBottom());
                
                vf.bringChildToFront(prevView);
                prevView.setVisibility(View.VISIBLE);
                    
            }
            if ( flipMode == FLIPMODE_NEXT)  {
                nextView.layout(fingerPosX , 
                          nextView.getTop(), 
                          fingerPosX + currentView.getWidth() + nextView.getWidth(), 
                          nextView.getBottom());
                
                vf.bringChildToFront(nextView);
                nextView.setVisibility(View.VISIBLE);
            }
            break;
            
        case MotionEvent.ACTION_UP:
            
            int activeIdx = -1;
            if ((this.startX - currentX) > movePageThreshold ) {
                activeIdx = nxtIdx;
            }else if 
                ((this.startX - currentX) < (movePageThreshold * -1) ) {
                activeIdx = preIdx;
            } else {
                activeIdx = curIdx;
            }
            int activeId = viewOrder[activeIdx];
            for(int i=0; i<vf.getChildCount(); i++) {
                // Log.i("MyApp",String.format("vf_id:%d,sel_id:%d",vf.getChildAt(i).getId(),activeId));
                if (vf.getChildAt(i).getId() == activeId) {
                    vf.setDisplayedChild(i);
                    break;
                }
            }
            flipMode = 0;
        default:
            break;
        }
        return true;
    }
    
    /**
     * 現在のページを移動することによってページをめくる
     * @param v
     * @param event
     * @return
     */
    public boolean pageFlipWithFingerMoveCurrent(View v, MotionEvent event) {
        float currentX = event.getX();
        
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();

            currentView = vf.getCurrentView();
            movePageThreshold = (currentView.getWidth() / 5);
            
            int viewCount = viewOrder.length;
            for (int i=0; i<viewCount; i++) {
                //Log.i("MyApp", "ord=id:" + viewOrder[i] + "==" + cv.getId());
                if (viewOrder[i] == currentView.getId()) {
                    curIdx = i;
                    break;
                }
            }
            
            if (curIdx >= 0) {
                preIdx = curIdx - 1;
                nxtIdx = curIdx + 1;
                preIdx = (preIdx < 0         )?viewCount - 1:preIdx;
                nxtIdx = (nxtIdx >= viewCount)?0            :nxtIdx;
                
                prevView = vf.findViewById(viewOrder[preIdx]);
                nextView = vf.findViewById(viewOrder[nxtIdx]);
            }
            
            Log.i(LOG_TAG, 
                    String.format("Pre=%d(%d),Cur=%d(%d),Nxt=%d(%d)" 
                                    ,preIdx,prevView.getId()
                                    ,curIdx,currentView.getId()
                                    ,nxtIdx,nextView.getId()));
            
            break;
        case MotionEvent.ACTION_MOVE:
            int travelDistanceX = (int)(currentX - this.startX);
            int fingerPosX = (int)currentX;
            
            if (flipMode == FLIPMODE_NOMOVE) {
                if (travelDistanceX > SWITCH_THRESHOLD) {
                    flipMode = FLIPMODE_PREV;
                } else if
                   ( travelDistanceX < (SWITCH_THRESHOLD * -1)  ) {
                    flipMode = FLIPMODE_NEXT;
                } else {
                    flipMode = FLIPMODE_NOMOVE;
                }
            }
            
            if (flipMode == FLIPMODE_PREV) {
                currentView.layout(fingerPosX, 
                            currentView.getTop(), 
                            fingerPosX + currentView.getWidth() , 
                            currentView.getBottom());
                
                vf.bringChildToFront(currentView);
                prevView.setVisibility(View.VISIBLE);
                    
            }
            if ( flipMode == FLIPMODE_NEXT)  {
                currentView.layout(fingerPosX - currentView.getWidth() , 
                          currentView.getTop(), 
                          fingerPosX, 
                          currentView.getBottom());
                
                vf.bringChildToFront(currentView);
                nextView.setVisibility(View.VISIBLE);
            }
            break;
            
        case MotionEvent.ACTION_UP:
            
            int activeIdx = -1;
            if ((this.startX - currentX) > movePageThreshold ) {
                activeIdx = nxtIdx;
            }else if 
                ((this.startX - currentX) < (movePageThreshold * -1) ) {
                activeIdx = preIdx;
            } else {
                activeIdx = curIdx;
            }
            int activeId = viewOrder[activeIdx];
            for(int i=0; i<vf.getChildCount(); i++) {
                // Log.i("MyApp",String.format("vf_id:%d,sel_id:%d",vf.getChildAt(i).getId(),activeId));
                if (vf.getChildAt(i).getId() == activeId) {
                    vf.setDisplayedChild(i);
                    break;
                }
            }
            flipMode = 0;
        default:
            break;
        }
        return true;
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#ffffff"
    android:id="@+id/layout_main"
    >
    <ViewFlipper android:id="@+id/details"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" android:animationCache="true">  
    </ViewFlipper>
</LinearLayout>

push_right_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" 
               android:toXDelta="100%p"
               android:duration="300"/>
    <alpha android:fromAlpha="1.0" 
           android:toAlpha="1.0" 
           android:duration="300" />
</set>

push_right_out.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" 
               android:toXDelta="0" 
               android:duration="300"/>
    <alpha android:fromAlpha="1.0" 
           android:toAlpha="1.0" 
           android:duration="300" />
</set>

テキストボックスに電話番号を入れてボタンを押すと、ダイヤル画面に遷移するサンプル。

コンタクトリストから、ユーザーID、表示名、電話番号をリストビューに表示し、選択されたらテキストボックスに電話番号を設定する。

telephone 

package info.typea.telephoneapp;

import android.app.ListActivity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;

/**
 * @see http://stackoverflow.com/questions/1721279/how-to-read-contacts-on-android-2-0
 */
public class TelephoneAppActivity extends ListActivity implements OnClickListener {
    /** Called when the activity is first created. */
    private EditText txtPhoneNo;
    private SimpleCursorAdapter adapter;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        txtPhoneNo = (EditText)findViewById(R.id.txt_phone_no);
        
        Button btn = (Button)findViewById(R.id.btn_call);
        btn.setOnClickListener(this);
    
        // リストに表示する項目
        String[] projection = {
                ContactsContract.CommonDataKinds.Phone._ID,
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                ContactsContract.CommonDataKinds.Phone.NUMBER
        };
        String   selection     = null;
        String[] selectionArgs = null;
        String   sortOrder = ContactsContract.PhoneLookup.DISPLAY_NAME + " asc";
        // カーソルの生成
        Cursor cur = managedQuery(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                                  projection, 
                                  selection, selectionArgs, sortOrder);

        // データベースのカラムとリストビューの関連づけ
        String from[] = new String[] { ContactsContract.CommonDataKinds.Phone._ID,
                                       ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                                       ContactsContract.CommonDataKinds.Phone.NUMBER };
        int[] to = new int[] { R.id.lbl_id, R.id.lbl_name, R.id.lbl_phone};
     
        // リストアダプタのセット
        adapter = new SimpleCursorAdapter(this, R.layout.row_phoneno, cur, from, to);
        setListAdapter(adapter);
    }
    
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);

        LinearLayout ll = (LinearLayout) v;
        TextView phone = (TextView) ll.findViewById(R.id.lbl_phone);
        
        txtPhoneNo.setText(phone.getText());
    }

    @Override
    public void onClick(View v) {

        // ダイアルアクティビティに遷移
        String phoneNo = "tel:" + txtPhoneNo.getText().toString();
        
        Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(phoneNo));
        startActivity(intent);
        
    }
}

/res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <EditText android:text="" 
                 android:layout_height="wrap_content" 
                 android:layout_width="fill_parent" 
                 android:inputType="phone" 
                 android:id="@+id/txt_phone_no"></EditText>
    <Button   android:id="@+id/btn_call" 
                 android:layout_height="wrap_content" 
                 android:layout_width="fill_parent" 
                 android:text="call"></Button>
    <ListView android:id="@+id/android:list" 
                 android:layout_height="wrap_content" 
                 android:layout_width="fill_parent"></ListView>
    <TextView android:text="No Contact list." 
                 android:id="@+id/android:empty" 
                 android:layout_width="wrap_content" 
                 android:layout_height="wrap_content"></TextView>
</LinearLayout>

/res/layout/row_phoneno.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 android:id="@+id/LinearLayout01" 
                 android:layout_width="fill_parent" 
                 android:layout_height="fill_parent">
    <TextView android:id="@+id/lbl_id" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" >
            </TextView>
    <TextView android:text=" : " 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" >
            </TextView>
    <TextView android:id="@+id/lbl_name" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" >
            </TextView>
    <TextView android:text=" : " 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" >
            </TextView>
    <TextView android:id="@+id/lbl_phone" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" >
            </TextView>
</LinearLayout>

あと、マニフェストで、以下のパーミッションを設定

<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

実機を使ってデバッグすることを覚えたので、Desire に搭載されているセンサーの値を取得してみよう。

http://developer.android.com/reference/android/hardware/SensorEvent.html

SenserEvent API の座標系

desire 

OpenGLの座標系と同じであり、一般のコンピューター上のスクリーン座標(左上が原点)とは異なる。

3軸 加速度センサー(3-axis Accelerometer)

 

accel

m/s2(メートル毎秒毎秒)で、加速度を表す。

加速度の計測

  1. X軸加速度
  2. Y軸加速度
  3. Z軸加速度
  • 水平なテーブルに端末を置いて、左側を右に向けて押した場合、X軸加速度は、正の値になる。
  • 水平なテーブルに端末を置いた場合、加速度の値は、端末の加速度(0 m/s^2) 引く、重力(-9.81 m/s^2)で、 +9.81 となる。
  • 水平なテーブルに端末を置いて、空に向けて A m/s^2 の加速度をかけた場合、A + 9.81 となる。

3軸 磁界センサー(3-axis Magnetic field sensor)

magnetic 

磁界センサーは、いろいろな用途に利用できるようだが、自分にはさっぱり理解できません。

μT(マイクロテスラ)という単位で電磁波をはかるらしい。

某ピップエレキバンの類似商品が、うちにあり、パッケージを見ると 「100マイクロテスラ」 とうたっている。

pip

Desire の背面の左下あたりに近づけると、確かに 100マイクロテスラ を超える値を検知!

某エレキバンの真偽を確かめるのには使えそうだ。

傾きセンサー(Orientation sensor)

orientation

  1. 方位 ・・・ 0=北, 90=東, 180=南, 270=西 として、0 ~ 359 の角度
  2. ピッチ(Pitch) ・・・ X軸にそって、-180 ~ 180 まで回転。Z軸が、Y軸に向かう場合、正の値。
  3. ロール(Roll) ・・・ Y軸にそって、 -90 ~ 90 まで回転。X軸が、Z軸に向かう場合、正の値。

orientation

近接センサー(Proximity sensor)

proximity

物体が近づいたら距離を cm で検知する。

ただし、上記に対応しておらず、「近い」「遠い」のいずれかで計測するものもある。

Desire では、近い 0 、遠い 1 と計測されるようだ。

輝度センサー(Light sensor)

light

明るさをルクス(lux)で計測

 

上記のソースコード。

package info.typea.sensorlist;
package info.typea.sensorlist;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class SensorListActivity extends ListActivity {

    private final static String ACTIVE_SENSOR = "ActiveSensor";
    private SensorManager sensorManager = null;
    
    private List<SensorItem> sensorItems;
    private TextView txtResult = null;
    
    private int activeSensorType = 0;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        sensorManager = (SensorManager)this.getSystemService(SENSOR_SERVICE);
        List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
        
        sensorItems = new ArrayList<SensorItem>();
        for (Sensor sensor : sensors) {
            SensorItem sensorItem = new SensorItem();
            sensorItem.setSensorName(sensor.getName());
            sensorItem.setSensorType(sensor.getType());
            sensorItems.add(sensorItem);
        }
        ArrayAdapter<SensorItem> lstAdapter 
            = new ArrayAdapter<SensorItem>(this, R.layout.sensor_row, sensorItems);
        setListAdapter(lstAdapter);
    
        txtResult = (TextView) findViewById(R.id.txt_result);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        
        // プリファレンスに保存されたセンサーを復帰
        SharedPreferences pref = getPreferences(MODE_PRIVATE);
        int sensorType = pref.getInt(ACTIVE_SENSOR, 0);
        startSensorTrace(sensorType);
    }
    
    @Override
    protected void onPause() {
        
        // 選択されたセンサーをプリファレンスに待避
        if (this.activeSensorType != 0) {
            SharedPreferences pref = getPreferences(MODE_PRIVATE);
            Editor editor = pref.edit();
            editor.putInt(ACTIVE_SENSOR, this.activeSensorType);
            editor.commit();
        }
        super.onPause();
    }
    
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
    
        txtResult.setText("");
        
        // 選択されたセンサーのリスニングを行う
        int sensorType = ((SensorItem)l.getAdapter().getItem(position)).getSensorType();
        startSensorTrace(sensorType);
    }
        
    /**
     * センサータイプを指定してリスナーを登録
     * 指定されなかったセンサーのリスナーの登録解除
     * @param sensorType
     */
    private void startSensorTrace(int sensorType) {
        ListAdapter lstAdapter = this.getListAdapter();
        for (int i=0; i<lstAdapter.getCount(); i++) {
            SensorItem sensorItem = (SensorItem)lstAdapter.getItem(i);
            if (sensorItem.getSensorType() == sensorType) {
                Sensor sensor =    sensorManager.getDefaultSensor(sensorItem.getSensorType());
                // センサーリスニングの登録
                sensorManager.registerListener(
                        sensorItem.listener,sensor, SensorManager.SENSOR_DELAY_NORMAL);
                this.activeSensorType = sensorType;
            } else {
                // センサーリスニングの登録解除
                sensorManager.unregisterListener(sensorItem.listener);
            }
        }
    }
    
    /**
     * 加速度センサー
     */
    class AccelerometerSensorListner implements SensorEventListener {
        @Override
        public void onAccuracyChanged(Sensor arg0, int arg1) {
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            String r = String.format(
                        "--------------------------------\n" + 
                        "3-axis Accelerometer\n"+
                        "--------------------------------\n" +
                        "minus Gx on the x-axis : %f\n" +
                        "minus Gy on the y-axis : %f\n" +
                        "minus Gz on the z-axis : %f"
                        ,event.values[0]
                        ,event.values[1]
                        ,event.values[2]
                      );
            txtResult.setText(r);
        }
    }
    /**
     * 傾きセンサー 
     * @see SensorManager.getOrientation()
     */
    class OrientationSensorListener implements SensorEventListener {
        @Override
        public void onAccuracyChanged(Sensor arg0, int arg1) {
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            String r = String.format(
                    "--------------------------------\n" + 
                    "Orientation sensor\n"+
                    "--------------------------------\n" +
                    "Azimuth(0=N,90=E,180=S,270=W) : %f\n" +
                    "Pitch (X axis -180 to 180) : %f\n" +
                    "Roll (Y axis -90 to 90) : %f"
                    ,event.values[0]
                    ,event.values[1]
                    ,event.values[2]
                  );
            txtResult.setText(r);
        }
    }
    /**
     * 磁界センサー 
     */
    class MagneticFieldSensorListener implements SensorEventListener {
        @Override
        public void onAccuracyChanged(Sensor arg0, int arg1) {
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            String r = String.format(
                    "--------------------------------\n" + 
                    "3-axis Magnetic field sensor\n"+
                    "--------------------------------\n" +
                    "X axis : %f μT\n" +
                    "Y axis : %f μT\n" +
                    "Z axis : %f μT"
                    ,event.values[0]
                    ,event.values[1]
                    ,event.values[2]
                  );
            txtResult.setText(r);
        }
    }
    
    /**
     * 輝度センサー
     */
    class LightSensorListener implements SensorEventListener {
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }

        @Override
        public void onSensorChanged(SensorEvent event) {
            String r = String.format(
                    "--------------------------------\n" + 
                    "light sensor\n"+
                    "--------------------------------\n" +
                    "%f lux"
                    ,event.values[0]
                  );
            txtResult.setText(r);
        }
    }
    
    /**
     * 近接センサー
     */
    class ProximitySensorListener implements SensorEventListener {
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }

        @Override
        public void onSensorChanged(SensorEvent event) {
            String r = String.format(
                    "--------------------------------\n" + 
                    "proximity sonsor\n"+
                    "--------------------------------\n" +
                    "%f cm"
                    ,event.values[0]
                  );
            txtResult.setText(r);
        }
        
    }
    
    /**
     * @see http://developer.android.com/reference/android/hardware/SensorEvent.html
     */
    class SensorItem {
        private int sensorType;
        private String sensorName;
        public SensorEventListener listener;
        
        public int getSensorType() {
            return sensorType;
        }

        /**
         * センサーごとにリスナーを生成
         * @param sensorType
         */
        public void setSensorType(int sensorType) {
            this.sensorType = sensorType;
            
            switch(this.sensorType) {
            case Sensor.TYPE_ACCELEROMETER:
                this.listener = new AccelerometerSensorListner();
                break;
            case Sensor.TYPE_ORIENTATION:
                this.listener = new OrientationSensorListener();
                break;
            case Sensor.TYPE_MAGNETIC_FIELD:
                this.listener = new MagneticFieldSensorListener();
                break;
            case Sensor.TYPE_LIGHT:
                this.listener = new LightSensorListener();
                break;
            case Sensor.TYPE_PROXIMITY:
                this.listener = new ProximitySensorListener();
                break;
            default:
                this.listener = new SensorEventListener() {
                    @Override
                    public void onSensorChanged(SensorEvent event) {
                    }
                    @Override
                    public void onAccuracyChanged(Sensor sensor, int accuracy) {
                        txtResult.setText("No Sensor Listener.");
                    }
                };
                break;
            }
        }
        public String getSensorName() {
            return sensorName;
        }
        public void setSensorName(String sensorName) {
            this.sensorName = sensorName;
        }
        @Override
        public String toString() {
            return getSensorName();
        }
    }
}

Java static クラス

いまのところ、「Google Androidアプリケーション開発入門 画面作成からデバイス制御まで――基本機能の全容」 を教科書としてサンプルを実装したりしてるのですが、SQLite のサンプルで、インナークラスを static 宣言してるんですね。

Google のチュートリアルでも同様でした。

ちょっと、見慣れない記法だと思ったのですが、どう使い分けるのだろう?

ちょっと挙動を試してみました。

static_class

Bar クラスと、Foo クラスを作成。

Barクラス

package test;

public class Bar {
    private String m_str;
    public Bar() {
        m_str = "Bar";
    }
    class Ham {
        public String getValue() {
            // 外部クラスの静的でないメンバーが参照できる
            return "Ham | " + Bar.this.m_str;
        }
    }
    static class Spam {
        public String getValue() {
            // ② 外部クラスの静的でないメンバーは参照できない
            // return Bar.this.m_str;
            return "Spam";
        }
    }
    
    public void makeInstance() {
        // ① 同じようにインスタンスは作れる
        System.out.println(new Ham().getValue());
        System.out.println(new Spam().getValue());
    }
}

Fooクラス

package test;

public class Foo {
    public static void main(String[] args) {
        
        // ③ クラスからインスタンス化できない。インスタンス化してから
        // Bar.Ham h = new Bar.Ham();
        Bar.Ham h = new Bar().new Ham();
        
        // ④ クラスからインスタンス化できる
        // Bar.Spam s = new Bar().new Spam();
        Bar.Spam s = new Bar.Spam();
        
        System.out.println(h.getValue());
        System.out.println(s.getValue());
        
        new Bar().makeInstance();
    }
}

結果

Ham | Bar
Spam
Ham | Bar
Spam

 

static クラス なので、なんか、インスタンスが作れない!? みたいに思ってしまうが、そんなことはない。

それぞれ、アウタークラスから、インナークラスのインスタンスは作成できる。(①)

ただし、当たり前と言えば当たり前だが、sutatic クラスからは、アウタークラスの static でないメンバー変数にアクセスできない(②)

Bar のインナークラスの、Ham と Spam のスコープ指定をしていないので、同一パッケージからは参照できる。

それぞれ、を同一パッケージの Foo クラスから参照してみる。

static でないインナークラスをアウタークラス以外のクラスからインスタンス化するには、アウタークラスをインスタンス化する必要がある。(③)

しかs、static なインナークラスは、アウタークラス以外のクラスからインスタンス化するには、static メソッドを参照するのと同じ仕方でインスタンス化できる。(④)

staticのついた内部クラスは、パッケージのように使える」 とはそういう(④)意味か。

ちょっとしたクラス群をパッケージではなくて、クラスにまとめて使うには、有用そうだ。これが使い方の本命かしら。

static でない場合の書き方、

new Bar().new Ham();

これは、覚えにくいし、違和感あるが、

new Bar.Spam();

こっちの書き方なら自然だ。

Softbank Desire を予約するも、まだ入荷せず。5月末には入荷するのかな~?久々に物欲がわいてます。

Windows Phone 7 もちょっと興味を持ったけど、Android 見れば見るほど面白そうだ。

実機入手までに、ちょっとずつ準備をしていこう。

android_sensor01

センサーを使ったアプリケーションが非常に面白そうにおもっているので、まずは搭載センサーを取得してみる(エミュレータ上で・・・)

加速度センサーが取得できた。

SensorListActivity

package info.typea.sensorlist;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.ArrayAdapter;

public class SensorListActivity extends ListActivity {
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // センサーマネージャーは、Context経由で取得するようだ
    	SensorManager sensorManager = (SensorManager)this.getSystemService(SENSOR_SERVICE);
        // Sensor.TYPE_ALL を指定し有効なセンサーをすべて取得
    	List sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
 
	// ListView に表示させるため、センサーの名称でリストを作成し直す   	
    	List sensorNames = new ArrayList();
    	for (Sensor sensor : sensors) {
    		sensorNames.add(sensor.getName());
    	}
    	ArrayAdapter lstAdapter 
    		= new ArrayAdapter(this, R.layout.sensor_row, sensorNames);
    	
    	setListAdapter(lstAdapter);
    }
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <ListView android:id="@+id/android:list"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>
      <TextView android:id="@+id/android:empty"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="No Sensors!"/>
</LinearLayout>

res/layout/sensor_row.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/text1" xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

いじょ。