2hours

1日2時間でなにができるかな

標準トリミング機能の呼び出し

大きい画像だとうまく動かない


トリミング機能をintentで呼び出そうと思い「android トリミング intent」でググったら大体以下のようなサンプルが出てくる。

Intent intent = new Intent("com.android.camera.action.CROP");
intent.setData(uri);              // トリミングに渡す画像パス
intent.putExtra("outputX", 200);        // トリミング後の画像の幅
intent.putExtra("outputY", 200);        // トリミング後の画像の高さ
intent.putExtra("aspectX", 1);         // トリミング後の画像のアスペクト比(X)
intent.putExtra("aspectY", 1);         // トリミング後の画像のアスペクト比(Y)
intent.putExtra("scale", true);         // トリミング中の枠を拡大縮小させるか
intent.putExtra("return-data", true);      // トリミングしたデータを返すよ
startActivityForResult(intent, REQUEST_CROP_PICK);

これでトリミング機能を呼び出すことが出来て、以下のように返却されたデータを受け取れる。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
    case REQUEST_GET_CONTENT:
        break;
    case REQUEST_CROP_PICK:
        Bitmap bitmap = data.getExtras().getParcelable("data");
    }
}


これで動くには動く。
ただ問題があって大きい画像を扱うと何も返ってこなくなる・・・。
例えば壁紙用にoutpuX、outputYに480、800とか指定すると動かない。



調べてみた

でも壁紙設定アプリでは出来てるわけだから何か方法があるはず。
ってことで調べてみた。
壁紙設定アプリではこのパッケージを呼び出してたので、そのソースを見れば解決する(はず)。

com.cooliris.media.CropImage

調べた結果

カメラ扱う時も同じ(だったと思う)だけど、intentに大きい画像渡すと動かない。
ので、トリミング機能に「データを返せ」ではなく「データを保存してくれ」と指示して解決。

トリミングはOSのバージョンによって呼ぶものが変わるっぽい。
その部分のソースはどこかからコピペしたんですが、引用元がどこだか分からなくなった・・・。
ただGalaxyNexusでは動かなかったので、パッケージを追加してあります。

サンプル

下にギャラリーで画像を選択して、トリミングした画像をSDカードに保存した後に画面に表示するサンプルを(エラーハンドリングとか適当)。

package goodspeed.jp.ne.hatena.d.cropimage;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.widget.ImageView;

public class ImageCropSampleActivity extends Activity {
    
    private static final int PICK_PICUTER = 0;
    private static final int CROP_IMAGE = 1;
    
    private File mFile;
    private ImageView mImageView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mImageView = (ImageView)findViewById(R.id.image);
        
        // ギャラリー呼び出し
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_PICK);
        startActivityForResult(intent, PICK_PICUTER);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        
        if (requestCode == PICK_PICUTER && resultCode == RESULT_OK) {
                
            // ギャラリーで選択されたファイルパス
            Uri uri = data.getData();

            // トリミングがいくつかあるようなので、使えるものを使う
            PackageManager pm = this.getPackageManager();
            List<ApplicationInfo> list = pm.getInstalledApplications(0);
            String[] apps = {"com.android.gallery", "com.cooliris.media", "com.google.android.gallery3d"};
            String[] clss = {"com.android.camera.CropImage", "com.cooliris.media.CropImage", "com.android.gallery3d.app.CropImage"};
            
            int classtype = -1;
            for (ApplicationInfo ai : list) {
                String s1 = ai.packageName;
                if (apps[0].equals(s1)) {
                    classtype = 0;
                }
                if (apps[1].equals(s1)) {
                    classtype = 1;
                }
                if (apps[2].equals(s1)) {
                    classtype = 2;
                }
            }
            
            // トリミング呼び出し
            Intent intent = new Intent();
            if (classtype >= 0) {
                // 適当に保存するところ作る
                Date date = new Date();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
                String filename = sdf.format(date)+".png";
                String dirPath = Environment.getExternalStorageDirectory().getPath()+"/"+getString(R.string.app_name);
                
                File dir = new File(dirPath);
                if(!dir.exists()){
                    if(!dir.mkdirs()){
                        // ディレクトリ作れなかった
                    }
                }

                // 先にファイルを用意しておいて、トリミングさんにそこに保存してくれるように頼む
                mFile = new File(dirPath+"/"+filename);
                intent.setClassName(apps[classtype], clss[classtype]);
                intent.setData(uri);
                intent.putExtra("outputX", 800);
                intent.putExtra("outputY", 800);
                intent.putExtra("aspectX", 1);
                intent.putExtra("aspectY", 1);
                intent.putExtra("scale", true);
                intent.putExtra("noFaceDetection", true);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mFile));
                intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.name());
                
                startActivityForResult(intent, CROP_IMAGE);
            } else {
                // トリミング機能呼び出し失敗
            }
        }
        
        if (requestCode == CROP_IMAGE && resultCode == RESULT_OK){
            // トリミングした画像を表示
            InputStream inputStream;
            try {
                inputStream = new FileInputStream(mFile);
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                mImageView.setImageBitmap(bitmap);
            } catch (FileNotFoundException e) {
                // ファイルがなかった
            }
            
        }
    }
}

フォント設定追加


フォントが指定出来るように修正してみました。
が、失敗に終わりましたorz

方法

AppWidgetではfindViewByIdで個々のViewオブジェクトを取得することが出来ないので、
RemoteViewに対してsetTextViewText(viewのid, 文字列)とかってやってる訳です。


で、TextViewに対して用意されているメソッドがsetTexColorとsetTextViewTextしかないと。
つまり色と文字しか設定出来ないと・・・。





しょうがないからBitmapに背景と文字を描画してセットしてみた結果。


設定画面ではActivityのTextViewに対してフォント指定したのでキレイに表示された。



ウィジェットのほうは・・・もちろん指定したフォントは反映されるんだけど、
なんだかぼやけた感じになってしまってどうにもならん状態。


結果


この方法だとやりたいことは全て出来るけど、処理が重かったり、
見た目が悪かったりするので不採用ということに。


もう1個思いついた方法があるので試してみる。
これでダメなら仕様の見直しかな・・・。

ウィジェットに関する困ったこと


Androidの開発を始めて最初に適当に作り始めたのが「とけい」アプリなのですが・・・。
ただの時計表示ウィジェットだから簡単だべ?と思ったのが大きな間違いだったのかもしれない。

ウィジェットに使えるViewがかなり限定されている

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView


使えるのはこれだけらしい・・・。
独自カスタムViewが使えれば問題ないのだけど、何をやっても上記以外のViewは受けつけてくれなかった。


AppWidgetがすぐ死ぬ

他の方の時計ウィジェットもよく参考に見るのですが、やっぱりコメントに「止まる」「遅れる」の文字が。
元々最小30分毎の更新を行うように出来ているので、そんなもんと言われればしょうがない気もするけど・・・。
ユーザーさんからすれば時計が止まるとかアプリとして問題外なわけで。


OSが1.6になってから永遠に着いて回っている問題なのですが、解決策は分かっていません。
止まりにくい物にはなってきているとは思いますが・・・。


テキストに文字列と色しか指定出来ない

とけいアプリによく「フォント設定」や「文字サイズ設定」を追加して欲しいという声があります。
が、Androidウィジェットの仕様上、色々と壁があって簡単には実装出来ませんorz

フォント指定

前述の通り、プログラムからテキストに対して指定出来るのが「表示する文字列」と「文字色」のみ。
(setTextColorとsetTextViewText)


なので、文字に色/フォント/サイズなどを指定して、Bitmapとして書き出せば問題は解決出来ます。
が、問題点がいくつか。
・やっぱりBitmap生成やらを1分毎にやったりすると当然重い。
・フォントファイルをアプリに乗っけるのでサイズが大きくなる。
・今までテキストとして文字を書き出してたので、処理を大幅に変える必要がある。


じゃあ最初から文字を画像として用意して、それを表示すればいいんじゃないか?と思ったのですが、
そうすると今度は「フォントの種類×文字色の種類×0〜9の数字などの文字種類」の数だけ画像を用意しなければならない。
画像作るの苦手だしなぁ・・・。



ということで、フォント指定については諦め気味。
一度Bitmapで表示するサンプルアプリを作って配ってみようかなとも思っています。
何人かのユーザーさんに使ってみてもらって、実用に耐えられるようであれば本番も変えようかと。

ウィジェットで IntentFilterを使う2

以前の記事(http://d.hatena.ne.jp/good-speed/20100401/1270127190)で
WidgetProviderのonUpdateでIntentFilterを登録すると怒られると書きました。


そこで、WidgetConfigureでセットすると使えますと紹介しましたが、
この回避方法は使えないものであることが分かりました。
端末が再起動された場合に、再登録をすることが出来ないからです。


じゃあどうすんの?

context.getApplicationContext().registerReceiver(intent, filter, null, mHandler);

とWidgetProviderでやったら、あっさり出来てしまいましたorz

テストの状況

実機で再び停止しましたorz
そこで更に詳しく調べていると、現象を再現出来ることに気付きました。


端末に負荷が掛かるとプロセスが死ぬっぽい

という結論。
とにかく端末に負荷をかけまくると

05-13 01:23:05.677: INFO/ActivityManager(78): Low Memory: No more background processes.
05-13 01:23:30.557: INFO/ActivityManager(78): Process net.selfip.goodspeed.clock_widget (pid 325) has died.

というログを吐いていることに気付いた。
自分のアプリも含め、立ち上がっているアプリ全てが落ちまくった。


他のアプリはなぜそのまま停止されないのか

自分のアプリ以外で、同じようにプロセスが死んだにも関わらず動き続けるものが。
(そのまま死にっぱなしのもありましたが・・・)


何か回避方法はないかと探し回ってみると
http://www.swingingblue.net/mt/archives/002768.html


うーん・・・。


結論

どうも止まること自体は回避するのは難しそうです。
なので、「止まったとしてもその後また動くようにする」ことで回避することに。


その結果、高負荷を掛けてプロセスが死んだとしても、動き続けるようになりました。




また大幅に処理を書き換えました・・・。


前回の記事で

これまで複数個ウィジェットを配置した場合、最後に追加したウィジェットの設定が全てに適用されるようになっていましたが、
この先何かと問題が起こりそうなので、別々に管理するように変更しました。

と書きましたが、それもまた元に戻っている状態です。
まぁ時計を複数個配置することはないから使用上は問題ないとは思いますが・・・。



再びテストして、週末にアップデート出来たらいいなーと思ってます(あくまでも希望)。

ウィジェットでIntentFilterを使う


時計の秒数表示を諦めたことによって、更新処理の変更を行った。


WidgetProviderでIntentFilterをセットすると

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_TIME_TICK);
    context.registerReceiver(mIntentReceiver, filter, null , mHandler);
}
という感じでWidgetProviderのonUpdateでIntentFilterをセットすると。
Caused by: android.content.ReceiverCallNotAllowedException: IntentReceiver components are not allowed to register to receive intents
登録出来ないと怒られてしまう。


http://feather.cocolog-nifty.com/weblog/2009/11/androidaction_t.html
調べていたら、こちらの方も「うまく行かなかった」と書いていた。


色々試してみた結果

どうもWidgetProviderのcontextに対してIntentFilterをセットするとダメなようだったので、
WidgetConfigureでセットするようにしたら、うまく動くようになった。


WidgetConfigure
public void onClick(View v) {
    〜(略)〜
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_TIME_TICK);
    this.getApplicationContext().registerReceiver(WidgetProvider.getIntentReceiver(), filter, null, mHandler);
    〜(略)〜
}
設定画面の決定ボタンが押されたらIntent.ACTION_TIME_TICKを登録。

WidgetProvider
public static BroadcastReceiver getIntentReceiver() {
    return mIntentReceiver;
}

private final static BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        〜(更新処理)〜
    }
};
WidgetProviderにBroadcastReceiverを定義しておいて、更新処理を行う。
WidgetConfigureでこいつを使うためにgetterも用意。


これでウィジェットでもIntent.ACTION_TIME_TICKを使った更新処理が出来た。



果たしてこれが一般的なやり方かどうかは定かではないが・・・。


バージョンアップ行いました

とけいウィジェットのバージョンアップを行いました。


告知した通り前バージョンを非公開にし、別のアプリとして再登録しました。
証明書なくしたばかりに面倒なことになり申し訳ないですorz

前バージョン非公開前の状態


総ダウンロード数:396
アクティブインストール:43%
評価:★★★


公開からダウンロード数は一定に伸びていましたが、評価が星3つに落ちてからダウンロード数が減りました。
また、証明書なくしたというコメントを書き込んでからアンインストール率が上がった気がしますw

ver 1.0.2 について


詳細は コチラ にある通りです。

ウィジェットサイズを2パターン(4×1と2×1)選べるようにしておいたのですが、万が一同時に配置された場合に問題がある為、リリースしませんでした。
問題が解決出来れば、次のアップデートで盛り込むつもりです。


問題点と今後の展開

【問題点】
重いのが最大の問題・・・。
結局背景色と透過率から画像を生成してるのが原因。
アプリ作り始めた時から色々試してはいるんですが・・・解決出来ない問題。

updatePeriodMillisの最小値が途中から30分に変更されたことを考えると、そもそもAndroidウィジェット
頻繁に更新するようには出来ていないので、元々無理があるのかもしれない。
処理が重いと、このアプリ自体の動作も不安定になるし、他のアプリにも影響するので何とかしたい。


【今後について】
・仕様変更を検討
どうにも上記の問題点がクリア出来ないので、以下のような仕様変更で回避を考えてます。

①秒数表示をなくす。
 そのまんま、更新回数を減らして対応する。
 でも秒数表示があるからこそ使ってくれてるような人もいるのでどうしようかと・・・。

②透過率設定をなくす。
 現在は透過率設定があるので、設定に応じてBitmapを生成しているのですが、
 これをなくして、あらかじめ用意した画像のみにして処理を軽くする。


どっちにするべきか・・・。
現在は、正確な1秒を表示出来ない現状を考えると、秒数表示を無くすのが無難なのかなと思ってます。