[[Software/Android/アプリケーション開発テキスト]] *Chapter 3 : インテントによる外部アプリケーションとの連携 [#t05697a1] この章では画像リサイズツールの開発を通して、インテントによる外部アプリケーションとの連携について解説します。 このアプリケーションはインテントによりギャラリーやカメラアプリを呼び出し、そこから受け取った画像を指定サイズにリサイズしてギャラリーに保存を行います。 新規Androidアプリケーションプロジェクトを以下の設定で作成します。 -Project Nameに「IResizer」と入力します -Application Nameに「IResizer」と入力します -Package Nameに「com.beatcraft.iresizer」と入力します -Create Activityにチェックを入れ、「IResizerActivity」と入力します ~ ~ **3-1. アプリケーションのレイアウト作成 [#s1bc295b] 今回作成するアプリケーションでは、縦画面時と横画面時でそれぞれのレイアウトを用意します。図3-1が縦画面、図3-2が横画面のイメージになります。 CENTER:&ref(./03_01.png,); CENTER:図3-1~ ~ CENTER:&ref(./03_02.png,); CENTER:図3-2~ ~ ~ 黒い余白部分は他アプリケーションから受け取った画像をプレビュー表示する領域となり、その下、もしくは右に操作部分が配置されています。今回は操作部を別レイアウトとして作成し、それぞれのレイアウトで取り込んで利用することとします。 レイアウトを作成する前に利用する文字列リソースを定義しておきます。リソースのstrings.xmlをダブルクリックして開き、以下の文字列リソースを追加します。 <string name="select_image">画像を選択</string> <string name="take_photo">写真を撮影</string> <string name="label_w">横</string> <string name="label_h">縦</string> <string name="keep_ratio">比率を維持</string> <string name="save">保存</string> ~ まずは操作部のレイアウトを作成します。IResizerをクリックして選択し、メニューから「File > New > Other...」を選択します。"Select a wizard"ダイアログが表示されたら、「Android > Android XML Layout File」を選択して「Next」ボタンをクリックして下さい。 続く"New Android Layout XML File"でFileに「controller.xml」と入力し、「Finish」ボタンをクリックするとレイアウトリソースファイルが作成されます。作成したcontroller.xmlを開き、ボタンやテキストボックスを配置していきます。 左部のPaletteからForm Widgetsをクリックして展開し、「Button」をドラッグ&ドロップで配置します。配置したボタンをクリックし、レイアウタ下部の「Properties」で以下のように設定を変更します。 -Idに「@+id/SelectImage」と入力します。 -Textに「@string/select_image」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 同様にボタンをもう一つ追加して、以下のように設定を変更します。 -Idに「@+id/TakePhoto」と入力します。 -Textに「@string/take_photo」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 続いて横サイズの入力項目を配置していきます。「LinearLayout (Horizontal)」をドラッグ&ドロップで配置し、以下のように設定を変更します。 -Idに「@+id/WLayout」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 TextViewを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/WLabel」と入力します。 -Textに「@string/label_w」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout widthに「wrap_content」を指定します。 EditTextを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/WSize」と入力します。 -Input typeに「number」を指定します。 -Layout heightに「wrap_content」を指定します。 -Layout weightに「1」と入力します。 -Layout widthに「wrap_content」を指定します。 TextViewを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/WSuffix」と入力します。 -Textに「px」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout widthに「wrap_content」を指定します。 縦サイズの入力項目も横サイズ同様に配置していきます。「LinearLayout (Horizontal)」をドラッグ&ドロップで配置し、以下のように設定を変更します。 -Idに「@+id/HLayout」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 TextViewを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/HLabel」と入力します。 -Textに「@string/label_h」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout widthに「wrap_content」を指定します。 EditTextを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/HSize」と入力します。 -Input typeに「number」を指定します。 -Layout heightに「wrap_content」を指定します。 -Layout weightに「1」と入力します。 -Layout widthに「wrap_content」を指定します。 TextViewを上記レイアウトに追加し、以下のように設定を変更します。 -Idに「@+id/HSuffix」と入力します。 -Textに「px」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout widthに「wrap_content」を指定します。 続いてPaletteからForm Widgetsをクリックして展開し、「CheckBox」をドラッグ&ドロップで配置します。配置したチェックボックスの設定を以下のように変更します。 -Checkedに「true」を指定します。 -Idに「@+id/KeepRatio」と入力します。 -Textに「@string/keep_ratio」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 最後にもう一つボタンを追加します。 -Idに「@+id/Save」と入力します。 -Textに「@string/save」と入力します。 -Layout heightに「wrap_content」を指定します。 -Layout margin leftとLayout margin rightに「10dip」と入力します。 -Layout widthに「fill_parent」を指定します。 以上で操作部のレイアウトは完成です。完成後のアウトラインとレイアウトイメージ(図3-3)を下記に示します。 |LEFT:|CENTER:|c | LinearLayout&br; SelectImage&br; TakePhoto&br; WLayout&br; WLabel&br; WSize&br; WSuffix&br; HLayout&br; HLabel&br; HSize&br; HSuffix&br; KeepRatio&br; Save|&ref(./03_03.png,);&br;図3-3| ~ 縦画面用のレイアウトを作成していきましょう。プロジェクト作成時に自動生成されたmain.xmlを開き、「Hello World, IResizerActivity!」というテキストが設定されたTextViewを削除します。 次に左部のPaletteからImages & Mediaをクリックして展開し、「ImageView」をドラッグ&ドロップで配置します(この際に「Resource Chooser」というダイアログが表示された場合は「Clear」ボタンをクリックして下さい)。配置した画像ビューをクリックし、レイアウタ下部の「Properties」で以下のように設定を変更します。 -Adjust view boundsに「true」を指定します。 -Idに「@+id/Preview」と入力します。 -Scale typeに「fitCenter」を指定します。 -Layout heightに「0dip」と入力します。 -Layout marginに「5dip」と入力します。 -Layout weightに「1」と入力します。 -Layout widthに「fill_parent」を指定します。 ここで先ほど作成した操作部レイアウトを取り込みます。レイアウタ下部にある「main.xml」タブをクリックしてXMLエディタを表示し、上記ImageViewの下に以下を追加します。 <include android:layout_width="fill_parent" android:layout_height="wrap_content" layout="@layout/controller" /> これで先ほど作成した操作部のレイアウトが取り込まれ、図3-1のようなレイアウトになります(※)。~ ※Graphical Layoutタブで確認するとcontrollerリソースが無いとエラーが出ることがあります。この場合は一度アプリケーションを実行したあとにmain.xmlを閉じ、Eclipseを再起動すると正常に表示されます。 ~ 同様にして横画面用のレイアウトも作成します。まずはIResizerのresフォルダを右クリックし、メニューから「New > Folder」を選択し「layout-land」フォルダを作成します。このフォルダに配置されたレイアウトは、横画面時に優先的に利用されます。 layoutフォルダにあるmain.xmlをドラッグ&ドロップでlayout-landフォルダにコピーし、これを横画面用に変更していきます。 まずは最上位にあるLinearLayoutの設定を以下のように変更します。 -Orientationを「horizontal」に変更します。 続いてプレビュー用の画像ビュー(Preview)の設定を以下のように変更します。 -Layout heightを「fill_parent」に変更します。 最後に取り込んだcontrollerレイアウトの設定を以下のように変更します。 -Layout heightを「fill_parent」に変更します。 -Layout widthを「wrap_content」に変更します。 以上で横画面用レイアウトも完成です。Graphcal Layoutタブで図3-2のようになっていることを確認してください。 **3-2. ユーザーインターフェースの実装 [#w40a8ae8] 前項で作成したレイアウトをアクティビティに反映し、ボタンやテキストボックスに処理を実装していきます。 IResizerActivity.javaを開き、IResizerActivityに以下のメンバ変数を追加します。 private ImageView mPreview; private EditText mWSize; private EditText mHSize; private Bitmap mBitmap = null; private int mOrgW = 0; private int mOrgH = 0; ImageViewとEditText、Bitmapクラスを利用可能にするためにはパッケージのimportが必要になります。Eclipseのエディタでクラス名の上にマウスカーソルを置き、図3-4のようなポップアップメニューが表示されたら「import 'ImageView' (android.widget)」行をクリックすると自動的にそのクラスのパッケージimportが挿入されます。 CENTER:&ref(./03_04.png,); CENTER:図3-4~ ~ mBitmap、mOrgWとmOrgHの三つのメンバ変数は他のアプリケーションから取得した画像とそのサイズを保持するための物です。 続いて、このクラスで各ボタンのクリック処理を行うためandroid.view.ViewのimportとView.OnClickListenerのimplementsを追加します。 import android.view.View; public class IResizerActivity extends Activity implements View.OnClickListener { onCreateメソッドの後にonClickメソッドと二つのprivateメソッドを追加します(リスト3-1)。onClickメソッドの処理は後ほど実装するため、空のままにしておきます。 -リスト3-1 : @Override publiic void onClick(View view) { } private void setupUI() { setContentView(R.layout.main); Button button; button = (Button) findViewById(R.id.SelectImage); button.setOnClickListener(this); button = (Button) findViewById(R.id.TakePhoto); button.setOnClickListener(this); button = (Button) findViewById(R.id.Save); button.setOnClickListener(this); mPreview = (ImageView) findViewById(R.id.Preview); mWSize = (EditText) findViewById(R.id.WSize); mHSize = (EditText) findViewById(R.id.HSize); setPreview(); mWSize.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean flag) { if (flag == false) { CheckBox keepRatio = (CheckBox) findViewById(R.id.KeepRatio); if (keepRatio.isChecked() == true) { String base = mWSize.getText().toString().trim(); if ((base.equals("") == false) && (mOrgW != 0) && (mOrgH != 0)) { double h = 0; double w = Integer.parseInt(base); double ratio = (double) mOrgH / (double) mOrgW; h = new BigDecimal(w * ratio).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue(); if (h < 1.0) { h = 1.0; } mHSize.setText(String.valueOf((int) h)); } } } } }); mHSize.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean flag) { if (flag == false) { CheckBox keepRatio = (CheckBox) findViewById(R.id.KeepRatio); if (keepRatio.isChecked() == true) { String base = mHSize.getText().toString().trim(); if ((base.equals("") == false) && (mOrgW != 0) && (mOrgH != 0)) { double w = 0; double h = Integer.parseInt(base); double ratio = (double) mOrgW / (double) mOrgH; w = new BigDecimal(w * ratio).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue(); if (w < 1.0) { w = 1.0; } mWSize.setText(String.valueOf((int) w)); } } } } }); } private void setPreview() { if (mBitmap != null) { mOrgW = mBitmap .getWidth(); mOrgH = mBitmap .getHeight(); mWSize.setText(String.valueOf(mOrgW)); mHSize.setText(String.valueOf(mOrgH)); mPreview.setImageBitmap(mBitmap); } } ~ setupUIメソッドではまず、setContentViewでmainレイアウトをアクティビティに設定したあと、「画像を選択」「写真を撮影」「保存」ボタンをfindViewByIdで取得し、このアクティビティをOnClickListenerとして登録しています。 続いて画像プレビュー用のImageView、横/縦サイズ入力用EditTextを取得して保持しておき、setPreviewメソッドを呼び出しています。setPreviewメソッドでは他のアクティビティから受け取った画像がある場合、画像プレビュー用のImageViewに設定し、オリジナル画像サイズを保持しています。 その後横/縦サイズ入力用EditTextそれぞれにOnFocusChangeListenerを登録しています。ここでは「比率を維持」チェックボックスがチェックされていた場合に、リサイズ後の縦横比を可能な限り維持するための自動計算処理を行っています。~ onFocusChangeはそのウィジェットのフォーカス状態が変更される際に呼び出されるメソッドで、引数flagがfalseの場合はフォーカスを失ったことを示します。このタイミングで、入力された値にオリジナル画像サイズから算出された比率を掛け、四捨五入した物をもう一方のサイズとして設定しています。計算結果が0になることもありますが、この場合は1で上書きすることで最低限の画像サイズを確保しています。 四捨五入で使用したBigDecimalと「比率を維持」チェックボックスを保持するためのCheckBoxを利用可能とするため、次のimport行を追加します。 import java.math.BigDecimal; import android.widget.CheckBox; 以上の作成が完了したら、自動生成されたonCreateを以下のように変更します(リスト3-2)。 -リスト3-2 : /** Called when the activity is first created. */ @Override publiic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setupUI(); } ~ 元々のコードではここで直接setContentViewを呼び出していましたが、これをsetupUI呼び出しに変更しています。また、requestWindowFeatureメソッドにWindow.FEATURE_NO_TITLEを渡して呼び出すことにより、アクティビティのタイトルバーを非表示にしています。図3-5左がタイトルバーを表示、右が非表示としたものです。 CENTER:&ref(./03_05.png,); CENTER:図3-5~ ~ ここまでで一度アプリケーションを実行してみましょう。Android端末を傾けて、縦画面表示と横画面表示を切り替えてみて下さい。エミュレータで実行している場合はCtrl+F12で縦横切替を行うことが出来ます(Windowsの場合)。 **3-3. インテントによる外部アプリケーション呼び出し [#h775ca68] ここからは「画像を選択」ボタンと「写真を撮影」ボタンの処理を実装し、インテントで外部アプリケーションを呼び出す手順を解説します。 まずは外部アプリケーション呼び出しに使用する二つの定数と、カメラからのデータ受け取りに使用するメンバ変数をIResizerActivityクラスに追加します。 public static final int REQUEST_SELECT_IMAGE = 1234; public static final int REQUEST_TAKE_PHOTO = 1235; private Uri mUriFromCamera; 続いて前項で追加したonClickメソッドを以下のように変更します(リスト3-3)。 -リスト3-3 : @Override publiic void onClick(View view){ int id = view.getId(); switch (id) { case R.id.SelectImage: selectImage(); break; case R.id.TakePhoto: takePhoto(); break; } } ~ 引数に渡されたviewからgetIdメソッドでIDを取得し、「画像を選択」ボタンであればselectImage、「写真を撮影」ボタンであればtakePhotoメソッドの呼び出しを行っています。 これらのメソッドで使用する文字列リソースを追加しておきます。strings.xmlを開き以下を追加して下さい。 <string name="app_notfound">対応アプリケーションが見つかりません。</string> リスト3-4がselectImageメソッドになります。Intent、AlertDialog、DialogInterfaceを利用するためパッケージのimportも忘れずに行なって下さい。 -リスト3-4 : private void selectImage() { try { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, REQUEST_SELECT_IMAGE); } catch (Exception e) { AlertDialog.Builder builder = null; builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.app_notfound)); builder.setNeutralButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } } ); builder.show(); } } ~ tryブロックで囲まれた部分では、まずIntent.ACTION_PICKを引数にしたインテントを生成しています。コンテキストとクラスを直接指定したものと違い、"データ取得のため"といったような曖昧な指定になっていますが、こうして生成されたインテントでは対応可能なコンポーネントをAndroidが自動的に探し出してくれます。両者を区別するため、前者を「明示的なインテント」、後者を「暗黙的なインテント」と呼びます。 今回のアプリケーションでは画像データを必要とするため、インテントのsetTypeメソッドに「image/*」を渡して対象を絞り込んでいます。呼び出し先からのデータを受け取るため、startActivityではなくstartActivityForResultメソッドを使用し、この呼び出しに対する結果が返ってきたかどうかを判断するために先程追加した定数 "REQUEST_SELECT_IMAGE"を渡しています。 指定したインテントに対応可能なアクティビティをAndroidが見つけられなかった場合、ActivityNotFound例外が発生します。これに対応するため、catchブロックではAlertDialog.Builderを利用して警告ダイアログを生成し表示しています。 続いてtakePhotoメソッドも実装していきます(リスト3-5)。新たに使用するContentValues、MediaStoreのパケージimportも忘れずに行なって下さい。 -リスト3-5 : private void takePhoto() { try { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.MIME_TYPE, "image/*"); mUriFromCamera = getContentResolver().insert( MediaStore.Images.Media. EXTERNAL_CONTENT_URI, values); intent.putExtra(MediaStore.EXTRA_OUTPUT, mUriFromCamera); startActivityForResult(intent, REQUEST_TAKE_PHOTO); } catch (Exception e) { AlertDialog.Builder builder = null; builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.app_notfound)); builder.setNeutralButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } } ); builder.show(); } } ~ カメラを呼び出すインテントにはMediaStore.ACTION_IMAGE_CAPTUREを指定します。Intent.ACTION_PICKでは選択した画像をUriとして返してくれるのですが、カメラからはサムネイルのデータが拡張データとして返ってきます。実際の撮影データを同様に取り扱うため、"image/*"をMIMEタイプに設定したContentValuesをContentResolverに追加し、割り当てられたUriをインテントの拡張データMediaStore.EXTRA_OUTPUTとして追加すると、撮影した画像がこのUriに保存されるようになります。~ これを先程追加したメンバ変数mUriFromCameraに保持しておくことで、カメラから画像を取得する場合に参照出来るようにしています。結果がカメラから返ってきたことを判断するため、startActivityForResultには定数"REQUEST_TAKE_PHOTO"を渡しています。 再びアプリケーションを実行して「画像を選択」「写真を撮影」ボタンの動作を確認してみましょう。対応可能なコンポーネントが複数見つかった場合、図3-6のようなダイアログが表示されますが、基本的にどちらを選んでも問題ありません。 CENTER:&ref(./03_06.png,); CENTER:図3-6~ ~ **3-4. 外部アプリケーションからのデータ取得 [#a8315a05] 前項までで外部アプリケーションを呼び出すことが出来ました。ここからは外部アプリケーションから渡されたデータを受け取り、画面に反映するまでを実装していきます。 startActivityForResultで呼び出したアプリケーションからの結果はonActivityResultメソッドで受け取ることが出来ます(リスト3-6)。 -リスト3-6 : protected void onActivityResult(int reqCode, int result, Intent data) { super.onActivityResult(reqCode, result, data); Uri uri = null; if (result == RESULT_OK) { if (reqCode == REQUEST_SELECT_IMAGE) { uri = data.getData(); } else if (reqCode == REQUEST_TAKE_PHOTO) { uri = mUriFromCamera; } mBitmap = getImage(uri); setPreview(); } else if (reqCode == REQUEST_TAKE_PHOTO) { getContentResolver().delete(mUriFromCamera, null, null); } } ~ superクラスのonActivityResultを呼び出した後、resultをチェックしています。これがRESULT_OKの場合、呼び出したアプリケーションがデータを返してくれたことを示します。~ reqCodeにはstartActivityResultを呼び出した際のリクエストコードが入っています。これが"REQUEST_SELECT_IMAGE"であれば「画像を選択」での呼び出しとなり、渡されたインテントからgetDataメソッドでUriを取得出来ます。~ 一方"REQUEST_TAKE_PHOTO"の場合は「写真を撮影」での呼び出しになりますので、予め保持しておいたmUriFromCameraを使用しています。また、resultがRESULT_OKでなかった場合はContentResolverのdeleteメソッドでこのUriを削除しています。 取得したUriを後ほど実装するgetImageメソッドに引き渡してビットマップ画像を取得し、setPreviewメソッドでプレビュー用ImageViewへの設定と画像サイズの取得を行います。 リスト3-7がgetImageメソッドになります。InputStream、BitmapFactoryパッケージのimportも併せて行います。 -リスト3-7 : private Bitmap getImage(Uri uri) { try { InputStream is = getContentResolver().openInputStream(uri); return BitmapFactory.decodeStream(is); } catch (Exception e) { e.printStackTrace(); return null; } } ~ ContentResolverのopenInputStreamメソッドにUriを渡すと、そのUriにあるファイルのInputStreamが取得出来ます。これをBitmapFactoryのdecodeStreamに引渡し、Bitmapオブジェクトをデコードとして返しています。指定のUriにファイルがなかった場合にopenInputStreamメソッドがFileNotFoundException例外を発行するので、catchブロックではこれを受けてnullを返します。 ContentResolverのopenInputStreamメソッドにUriを渡すと、そのUriにあるファイルのInputStreamが取得出来ます。これをBitmapFactoryのdecodeStreamに引渡し、Bitmapオブジェクトをデコードして返しています。指定のUriにファイルがなかった場合にopenInputStreamメソッドがFileNotFoundException例外を発行するので、catchブロックではこれを受けてnullを返します。 以上でデータ受け取りの実装は完了ですが、Androidのバージョンによってはインテントの発行後アプリケーションで画面の縦横切替が起きると、元のアクティビティが結果を受け取れなくなることがあります。これは縦横切替によって一度アクティビティが破棄される(onDestroyが呼ばれる)ためで、これを避けるには縦横切替時にアクティビティの破棄ではなく、コンフィギュレーション変更が発生するようにします。 IResizerのマニフェストファイル(AndroidManifest.xml)を開き、android:configChanges属性をIResizerActivityに追加します(リスト3-8)。 -リスト3-8 : <activity android:configChanges="orientation" android:label="@string/app_name" android:name=".IResizerActivity" > ~ 続いてIResizerActivity.javaにonConfigurationChangedメソッドを追加します(リスト3-9)。 -リスト3-9 : @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setupUI(); } ~ superクラスのonConfigurationChangedを呼び出したあと、setupUIでレイアウトの設定を改めて行なっています。これをしないと(アクティビティが破棄されないため)切替前のレイアウトがそのままになってしまいます。 ~ **3-5. リサイズ画像保存処理の実装 [#af5fc665] ここからは受け取った画像をリサイズし、保存するまでの処理を実装していきます。保存に際しての仕様は以下の通りとします。 -フォーマットはJPEG固定とする。 -ファイル名は現在時刻を用いて「YYYYMMDD_HHMMSS.jpg」という形式で自動生成する。 -保存先はカメラフォルダに固定とし、ギャラリーに表示されるよう登録を行う。 -保存前に確認ダイアログを表示する。 まずは保存処理で使用する文字列リソースを追加しておきます。strings.xmlを開き、以下を追加して下さい。 <string name="cancel">取消</string> <string name="save_title">画像保存</string> <string name="filename_label">ファイル名:</string> <string name="isize_label">画像サイズ:</string> <string name="save_desc">で保存します。</string> <string name="save_success">保存しました</string> <string name="save_failed">保存に失敗しました</string> 続いて確認ダイアログ用のレイアウトを作成していきます。IResizerのレイアウトリソースフォルダに以下の内容でdialog.xmlというレイアウトファイルを作成します(リスト3-10)。 -リスト3-10 : <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/FileNameLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" > <TextView android:id="@+id/FileLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/filename_label" /> <TextView android:id="@+id/FileName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_weight="1" /> </LinearLayout> <LinearLayout android:id="@+id/ImageSizeLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" > <TextView android:id="@+id/SizeLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/isize_label" /> <TextView android:id="@+id/ImageSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_weight="1" /> </LinearLayout> <TextView android:id="@+id/Description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:gravity="right" android:text="@string/save_desc" /> <LinearLayout android:id="@+id/ButtonLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center_horizontal" > <Button android:id="@+id/ExecSave" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/save" /> <Button android:id="@+id/Cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/cancel" /> </LinearLayout> </LinearLayout> ~ ダイアログのアウトラインとレイアウトイメージは以下のようになります(図3-7)。 |LEFT:|CENTER:|c | LinearLayout&br; FileNameLayout&br; FileLabel&br; FileName&br; ImageSizeLayout&br; SizeLabel&br; ImageSize&br; Description&br; ButtonLayout&br; ExecSave&br; Cancel|&ref(./03_07.png,);&br;図3-7| ~ 保存時のファイル名とダイアログを保持するため、IResizerActivityに以下のメンバ変数を追加します。 private String mFileName = ""; private Dialog mDialog = null; IResizerActivityの「保存」ボタンに応答するため、onClickのswitchに以下のcaseを追加します。 case R.id.Save: showConfirmDialog(); break; リスト3-11がshowConfirmDialogメソッドになります。DateFormat、TextViewのパッケージimportも追加して下さい。 -リスト3-11 : private void showConfirmDialog() { if ((mBitmap != null) && (mWSize.getText().toString().trim().equals("") == false) && (mHSize.getText().toString().trim().equals("") == false)) { long ctime = System.currentTimeMillis(); mFileName = DateFormat.format("yyyyMMdd_kkmmss", ctime).toString() + ".jpg"; mDialog = new Dialog(this); mDialog.setContentView(R.layout.dialog); mDialog.setTitle(getString(R.string.save_title)); Button button; button = (Button) mDialog.findViewById(R.id.ExecSave); button.setOnClickListener(this); button = (Button) mDialog.findViewById(R.id.Cancel); button.setOnClickListener(this); TextView tv; tv = (TextView) mDialog.findViewById(R.id.FileName); tv.setText(mFileName); tv = (TextView) mDialog.findViewById(R.id.ImageSize); tv.setText(mWSize.getText().toString() + "x" + mHSize.getText().toString()); mDialog.show(); } } ~ リサイズ元画像とリサイズ解像度が空でないことを確認し、現在日時から「YYYYMMDD_HHMMSS.jpg」形式のファイル名を作成、保持しておきます。~ 次にDialogオブジェクトを生成して先ほどのレイアウトを割り当て、「保存」「取消」両ボタンのOnClickListenerをIResizerActivityに、生成したファイル名とリサイズ後の画像サイズをダイアログの各項目にセットしてダイアログを表示します。 ダイアログの両ボタンに応答するため、onClickのswitchに以下のcaseを追加します。 case R.id.ExecSave: case R.id.Cancel: if (mDialog != null) { if (id == R.id.ExecSave) { execSave(); } mDialog.dismiss(); mDialog = null; } break; ~ 「保存」「取消」両ボタンとも、ダイアログがnullでなければdismissメソッドで破棄しています。IDがExecSave、すなわち「保存」ボタンの場合には実際の保存処理を行うexecSaveを呼び出します。~ リスト3-12がexecSaveメソッドになります。Toastのパッケージimportも忘れずに行なって下さい。 -リスト3-12 : private void execSave() { int w = Integer.parseInt(mWSize.getText().toString()); int h = Integer.parseInt(mHSize.getText().toString()); Bitmap bitmap = Bitmap.createScaledBitmap(mBitmap, w, h, true); try { MediaStore.Images.Media.insertImage(getContentResolver(), bitmap, mFileName, null); Toast.makeText(this, getString(R.string.save_success), Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, getString(R.string.save_failed), Toast.LENGTH_SHORT).show(); } } ~ 横/縦サイズ用EditTextからサイズを取得したら、Bitmap.createScaledBitmapに元画像とサイズを引き渡してリサイズ画像を生成します。~ 生成した画像をMediaStore.Images.Media.insertImageメソッドに引き渡すとカメラフォルダに画像が保存され、ContentResolverによりギャラリーにも表示されるよう登録されます。このメソッドはFileNotFoundExeception例外を発行する可能性があるためtryブロックで囲み、catchブロックでは保存失敗時のメッセージをToastで表示するようにしています。 これでリサイズ画像を保存することが出来るようになりました。図3-8のようなダイアログが表示されることを確認し、実際に保存してみましょう。 CENTER:&ref(./03_08.png,); CENTER:図3-8~ ~ 保存に失敗する場合、保存先が外部ストレージになっている可能性があります。IResizerのマニフェストファイルを開いて、次のパーミッションを追加して下さい。 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ~ **3-6. 共有機能による他アプリケーションからの呼び出し [#k99c8e4c] 前項まででインテントを利用して外部アプリケーションと連携し、データのやり取りを行うことが出来るようになりましたが、もう少し進めて、Androidの共有機能を利用して他の画像アプリケーションからIResizerへ画像を送信出来るようにしてみましょう。 画像の共有先としてIResizerを選択可能にするには、アクティビティにandroid.intent.action.SENDインテントフィルターを追加する必要があります。IResizerのマニフェストファイルを開き、IResizerActivityに以下を追加して下さい。 <intent-filter > <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <action android:name="android.intent.action.SEND" /> </intent-filter> ~ これで画像アプリケーションの共有メニューにIResizerが表示されるようになります。引き続き外部からの共有を処理出来るよう、IResizerActivity.javaに変更を加えていきます。IResizerActivityのonCreateメソッドでsetupUIを呼び出した後に、以下のように追加して下さい。 if(Intent.ACTION_SEND.equals(getIntent().getAction())) { Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); mBitmap = getImage(uri); setPreview(); } ~ これで完成です。ギャラリーなどの共有機能を持った画像アプリを起動し、共有メニューにIResizerが表示されるのを確認してみましょう(図3-9)。 CENTER:&ref(./03_09.png,); CENTER:図3-9~ ~ **3-7. IResizer全ソースコード/XMLファイル [#mbd93e73] -リスト3-13 IResizerActivity.java : package com.beatcraft.iresizer; import java.io.InputStream; import java.math.BigDecimal; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentValues; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; import android.text.format.DateFormat; import android.view.View; import android.view.Window; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; public class IResizerActivity extends Activity implements View.OnClickListener { public static final int REQUEST_SELECT_IMAGE = 1234; public static final int REQUEST_TAKE_PHOTO = 1235; private ImageView mPreview; private EditText mWSize; private EditText mHSize; private Bitmap mBitmap = null; private int mOrgW = 0; private int mOrgH = 0; private Uri mUriFromCamera; private String mFileName = ""; private Dialog mDialog = null; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setupUI(); if (Intent.ACTION_SEND.equals(getIntent().getAction())) { Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); mBitmap = getImage(uri); setPreview(); } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setupUI(); } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.SelectImage: selectImage(); break; case R.id.TakePhoto: takePhoto(); break; case R.id.Save: showConfirmDialog(); break; case R.id.ExecSave: case R.id.Cancel: if (mDialog != null) { if (id == R.id.ExecSave) { execSave(); } mDialog.dismiss(); mDialog = null; } break; } } protected void onActivityResult(int reqCode, int result, Intent data) { super.onActivityResult(reqCode, result, data); Uri uri = null; if (result == RESULT_OK) { if (reqCode == REQUEST_SELECT_IMAGE) { uri = data.getData(); } else if (reqCode == REQUEST_TAKE_PHOTO) { uri = mUriFromCamera; } mBitmap = getImage(uri); setPreview(); } else if (reqCode == REQUEST_TAKE_PHOTO) { getContentResolver().delete(mUriFromCamera, null, null); } } private void setupUI() { setContentView(R.layout.main); Button button; button = (Button) findViewById(R.id.SelectImage); button.setOnClickListener(this); button = (Button) findViewById(R.id.TakePhoto); button.setOnClickListener(this); button = (Button) findViewById(R.id.Save); button.setOnClickListener(this); mPreview = (ImageView) findViewById(R.id.Preview); mWSize = (EditText) findViewById(R.id.WSize); mHSize = (EditText) findViewById(R.id.HSize); setPreview(); mWSize.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean flag) { if (flag == false) { CheckBox keepRatio = (CheckBox) findViewById(R.id.KeepRatio); if (keepRatio.isChecked() == true) { String base = mWSize.getText().toString().trim(); if ((base.equals("") == false) && (mOrgW != 0) && (mOrgH != 0)) { double h = 0; double w = Integer.parseInt(base); double ratio = (double) mOrgH / (double) mOrgW; h = new BigDecimal(w * ratio).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue(); if (h < 1.0) { h = 1.0; } mHSize.setText(String.valueOf((int) h)); } } } } }); mHSize.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean flag) { if (flag == false) { CheckBox keepRatio = (CheckBox) findViewById(R.id.KeepRatio); if (keepRatio.isChecked() == true) { String base = mHSize.getText().toString().trim(); if ((base.equals("") == false) && (mOrgW != 0) && (mOrgH != 0)) { double w = 0; double h = Integer.parseInt(base); double ratio = (double) mOrgW / (double) mOrgH; w = new BigDecimal(h * ratio).setScale(0, BigDecimal.ROUND_HALF_UP).doubleValue(); if (w < 1.0) { w = 1.0; } mWSize.setText(String.valueOf((int) w)); } } } } }); } private void selectImage() { try { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, REQUEST_SELECT_IMAGE); } catch (Exception e) { AlertDialog.Builder builder = null; builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.app_notfound)); builder.setNeutralButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.show(); } } private void takePhoto() { try { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.MIME_TYPE, "image/*"); mUriFromCamera = getContentResolver().insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); intent.putExtra(MediaStore.EXTRA_OUTPUT, mUriFromCamera); startActivityForResult(intent, REQUEST_TAKE_PHOTO); } catch (Exception e) { AlertDialog.Builder builder = null; builder = new AlertDialog.Builder(this); builder.setMessage(getString(R.string.app_notfound)); builder.setNeutralButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.show(); } } private void showConfirmDialog() { if ((mBitmap != null) && (mWSize.getText().toString().trim().equals("") == false) && (mHSize.getText().toString().trim().equals("") == false)) { long ctime = System.currentTimeMillis(); mFileName = DateFormat.format("yyyyMMdd_kkmmss", ctime).toString() + ".jpg"; mDialog = new Dialog(this); mDialog.setContentView(R.layout.dialog); mDialog.setTitle(getString(R.string.save_title)); Button button; button = (Button) mDialog.findViewById(R.id.ExecSave); button.setOnClickListener(this); button = (Button) mDialog.findViewById(R.id.Cancel); button.setOnClickListener(this); TextView tv; tv = (TextView) mDialog.findViewById(R.id.FileName); tv.setText(mFileName); tv = (TextView) mDialog.findViewById(R.id.ImageSize); tv.setText(mWSize.getText().toString() + "x" + mHSize.getText().toString()); mDialog.show(); } } private void execSave() { int w = Integer.parseInt(mWSize.getText().toString()); int h = Integer.parseInt(mHSize.getText().toString()); Bitmap bitmap = Bitmap.createScaledBitmap(mBitmap, w, h, true); try { MediaStore.Images.Media.insertImage(getContentResolver(), bitmap, mFileName, null); Toast.makeText(this, getString(R.string.save_success), Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, getString(R.string.save_failed), Toast.LENGTH_SHORT).show(); } } private Bitmap getImage(Uri uri) { try { InputStream is = getContentResolver().openInputStream(uri); return BitmapFactory.decodeStream(is); } catch (Exception e) { e.printStackTrace(); return null; } } private void setPreview() { if (mBitmap != null) { mOrgW = mBitmap.getWidth(); mOrgH = mBitmap.getHeight(); mWSize.setText(String.valueOf(mOrgW)); mHSize.setText(String.valueOf(mOrgH)); mPreview.setImageBitmap(mBitmap); } } } ~ -リスト3-14 controller.xml : <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/SelectImage" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="@string/select_image" /> <Button android:id="@+id/TakePhoto" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="@string/take_photo" /> <LinearLayout android:id="@+id/WLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" > <TextView android:id="@+id/WLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/label_w" /> <EditText android:id="@+id/WSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="number" > <requestFocus /> </EditText> <TextView android:id="@+id/WSuffix" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="px" /> </LinearLayout> <LinearLayout android:id="@+id/HLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" > <TextView android:id="@+id/HLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/label_h" /> <EditText android:id="@+id/HSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="number" > </EditText> <TextView android:id="@+id/HSuffix" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="px" /> </LinearLayout> <CheckBox android:id="@+id/KeepRatio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:checked="true" android:text="@string/keep_ratio" /> <Button android:id="@+id/Save" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="@string/save" /> </LinearLayout> ~ -リスト3-15 layout/main.xml : <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <ImageView android:id="@+id/Preview" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_margin="5dip" android:layout_weight="1" android:adjustViewBounds="true" android:scaleType="fitCenter" /> <include android:layout_width="fill_parent" android:layout_height="wrap_content" layout="@layout/controller" /> </LinearLayout> ~ -リスト3-16 layout-land/main.xml : <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="horizontal" > <ImageView android:id="@+id/Preview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_margin="5dip" android:layout_weight="1" android:adjustViewBounds="true" android:scaleType="fitCenter" /> <include android:layout_width="wrap_content" android:layout_height="fill_parent" layout="@layout/controller" /> </LinearLayout> ~ -リスト3-17 dialog.xml : <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/FileNameLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" > <TextView android:id="@+id/FileLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/filename_label" /> <TextView android:id="@+id/FileName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_weight="1" /> </LinearLayout> <LinearLayout android:id="@+id/ImageSizeLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" > <TextView android:id="@+id/SizeLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/isize_label" /> <TextView android:id="@+id/ImageSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_weight="1" /> </LinearLayout> <TextView android:id="@+id/Description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:gravity="right" android:text="@string/save_desc" /> <LinearLayout android:id="@+id/ButtonLayout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center_horizontal" > <Button android:id="@+id/ExecSave" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/save" /> <Button android:id="@+id/Cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/cancel" /> </LinearLayout> </LinearLayout> ~ -リスト3-18 AndroidManifest.xml : <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.beatcraft.iresizer" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="7" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:configChanges="orientation" android:label="@string/app_name" android:name=".IResizerActivity" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter > <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <action android:name="android.intent.action.SEND" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> </manifest> ~ ---- RIGHT:内藤