「ピクセル密度」ってナニよ?

 さて。
 端末でアプリを実行すると、「画像表示(View)が拡大されてしまう」不具合について。
 (参照:「画面サイズ設定」ってナニよ?)
 原因は、画面サイズだと思ったんだけど、そうではなかったみたい。
 振り出しに戻る…。
 でも、ヒントはあった。
 それが「ピクセル密度」

「ピクセル密度」ってナニよ?

 ピクセル密度は、「1dotを何pixelで表すか?」というもの。
 何の役に立つのかというと、「表示されるものは、どの環境でも、同じ大きさで表示される」。
 17インチモニターでも、15インチモニターでも、そこに表示されるものは、同じ大きさになるってワケ。

 じゃ、なんで、画像が拡大表示しまうのか?
 それは画面と画像のピクセル密度が合っていないから。
 画像のピクセル密度が、画面の密度より低いため、拡大表示されてしまうんだ。
 PhotoShopやGIMPなんかで、印刷解像度を低くした時と似てる。
 「ピクセル密度 = 印刷解像度」みたいに捉えるとわかりやすいかもね。
 Androidでの表示は、WYSWYGの仕組みに近いんだ。

ピクセル密度を調べる

 ピクセル密度は、下記の方法で調べられる。
 実行すると、ログにピクセル密度が書き出されるよ。

	DisplayMetrics metrics = getResources().getDisplayMetrics();

	Log.d( "TEST", "density=" + metrics.density );

 DisplayMetrics では、画面に関する他の情報も取得できる。

	DisplayMetrics metrics = getResources().getDisplayMetrics();
	
	Log.d( "TEST", "density=" + metrics.density );
	Log.d( "TEST", "densityDpi=" + metrics.densityDpi );
	Log.d( "TEST", "scaledDensity=" + metrics.scaledDensity );
	Log.d( "TEST", "widthPixels=" + metrics.widthPixels );
	Log.d( "TEST", "heightPixels=" + metrics.heightPixels );
	Log.d( "TEST", "xDpi=" + metrics.xdpi );
	Log.d( "TEST", "yDpi=" + metrics.ydpi );

 ちなみに。
 「画面サイズ設定ってナニよ?」で画面サイズを調べた時、画面サイズは 533×400 だった。
 それぞれに、ピクセル密度:1.5 をかけると、800×600 になる。
 ちょうど、normalScreens での値とlargeScreens での値になるね。

画像リソースのピクセル密度設定(画像解像度設定)

 拡大表示される原因は、ピクセル密度の違いにあることがわかった。
 じゃ、画面のピクセル密度に、画像のピクセル密度を合わせればいいワケだ。
 でもソレってどうやるの…?

 印刷解像度をいじくったリソースを使用してもダメ。
 ピクセル密度は、画像リソースだけで行う設定じゃないんだ。

 ピクセル密度の設定は、Eclipseプロジェクトでも行うんだ。
 正確には、設定ではないんだけどね。(^_^;

 大抵、画像リソースは、drawable ディレクトリに置くよね?
 それと同じで、それぞれのピクセル密度のディレクトリに、最適化した画像リソースを置くんだ。

	drawable-ldpi … 120dpi( low )
	drawable-mdpi … 160dpi( medium )
	drawable-hdpi … 240dpi( high )

 たとえば、medium で533x400dot の画像の場合、drawable-hdpi に800x600dot に拡大した画像リソースを置く。
 そうすることで、動作環境がdensityDpi = 240(high)だったら、画像リソースはdrawable-hdpi から使用される。
 Android は動作環境に合わせて、それぞれから最適なピクセル密度のリソースを使用してくれるって仕組み。

 最適なリソースがない場合、Android が自動的に拡縮調整してくれる。
 ただし、基準はmedium(160dpi)。
 つまり、drawable にだけ置くと、リソースをmedium として拡縮処理をしてしまう。
 high(240dpi)の環境で表示すると、拡大表示されてしまうんだね。

 これは便利な仕組みなんだけど…ゲームなんかでは困った仕組みだよ。
 ゲームの場合、テレビのようにdpi調整はしないで欲しい…
 この場合、drawable-nodpi ディレクトリにリソースを置く。

	drawable-nodpi … dpi調整をしない

 今回、drawable ディレクトリにあったリソースを、drawable-nodpi へ移すことで、拡大表示されてしまう不具合は解消されたよ。

プログラムからピクセル密度(画像解像度)を設定する

 ピクセル密度は、リソース・ディレクトリで対応できることがわかった。
 でも、ソレってデータがリソースの時だけ。

 画像データを外部参照する時は…?

 外部参照だけじゃなく、プログラム側からピクセル密度を設定したい場合もあるよね。
 そこでピクセル密度を設定できるか、ざっと調べてみたよ。

◆Canvas

	int	 getDensity()
	void	 setDensity(int density)

http://developer.android.com/reference/android/graphics/Canvas.html

◆Bitmap

	int 	getDensity()
	void 	setDensity(int density)

	int 	getScaledHeight(int targetDensity)
	int 	getScaledWidth(int targetDensity)

http://developer.android.com/reference/android/graphics/Bitmap.html

◆BitmapDrawable

	void	 setTargetDensity(int density)
	void	 setTargetDensity(DisplayMetrics metrics)
	void	 setTargetDensity(Canvas canvas)

http://developer.android.com/reference/android/graphics/drawable/BitmapDrawable.html

 以上からすると、今回の不具合には、Viewで使ってるCanvas にピクセル密度を設定すればいいみたい。
 そこでこんな風↓にしてみたよ。

	Canvas canvas;
	Bitmap scrImg = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 );
	
	scrImg.setDensity( 160 );
	Log.d("TEST", "scrImg density = " + scrImg.getDensity() );
	
	Canvas scrImg_canv = new Canvas( scrImg );
//	scrImg_canv.setDensity( 160 );
	Log.d("TEST", "Canvas density = " + scrImg_canv.getDensity() );

 Canvas に直接、設定するのではなく、元になるBitmap の密度を設定してみた。
 Bitmap の設定がCanvas に引き継がれるか、確かめたかったため。
 実行してみると、設定値はちゃんと引き継がれていた。

 表示する画像データは、drawable ディレクトリに置いた画像リソース。
 medium(160dpi) と認識されるので、setDensity で設定する値も160にしてある。

 以上で、拡大表示される不具合は解消できたよ。

 この設定は、他のことにも使えそう。
 たとえば、動的に値を変えて、拡縮操作の代わりにするとか。
 まぁ、setBounds の方が使い勝手はいいから、意味はないだろうけどね。(^_^;

Posted in Bitmap, ピクセル密度. Tags: , , , , , , , , . 「ピクセル密度」ってナニよ? はコメントを受け付けていません。 »

ZipFile と ZipEntry ~Androidでzipを解凍する

 いわゆるアドベンチャー・ゲームみたいに、複数のデータファイルを扱う場合、それらをひとつのファイルにまとめることがほとんどだよね。
 その理由は、機密性が第一なのかな?
 でも配布を考えた場合、ひとつにまとまっていた方が都合いいんだ。
 まず、コピーが早い。
 ファイルの取りこぼしがない。
 それと一概にはいえないけど、通信パケットを節約できる。

 で、ひとつにまとめるには、どうしたらいいか?
 色々と方法はあるけど、オイラはzipを使うことにしている。
 なぜかといえば、まとめるツールは作らなくていいし、取り出すルーチンも簡単ですむから。

 というワケで、zipからデータを取り出すmethod を作ってみたよ。

ZipFile と ZipEntry

 zip を扱うのに、使うclass は以下の2つ。

●ZipFile
http://developer.android.com/reference/java/util/zip/ZipFile.html

●ZipEntry
http://developer.android.com/reference/java/util/zip/ZipEntry.html

 ZipFile は、そのままZIP を抽象化したもの。
 ZipEntry は、ZIPの中のファイルを抽象化したもの。

 method を眺めると、ZipEntry と Fileクラス はなんか似ている。
 たぶん、「ZipFile はHD、ZipEntry はファイル」と見立ててるんだろうね。
 取り出すときもInputStream を使うから、ファイル操作と手順はそう変わらなそう。

 ちなみに、ZipInputStream というclass もある。
 今回は使わなかったけど、役割はZipFile と同じみたいだ。
 おそらく、ネットからZIPを取得した時、メモリー上にZIPがある時、なんかに使うんだろね。

ファイル一覧を取得する

 まず、中のファイル一覧を取得してみた。
 下記method は、ZIPのパスを指定すると、中のファイル一覧をString配列として返すよ。

	public int err = 0;	// エラーがあったか? // -1 : エラー有り | other : エラーなし

	public String[] getList( String zipPath ){
		err = 0;
		String[] result = null;
		//--

		try{
			ZipFile zipFile = new ZipFile( zipPath );
			
			List<String> list = new ArrayList<String>();
			Enumeration<? extends ZipEntry>	enu = zipFile.entries();
			while( enu.hasMoreElements() ){
				ZipEntry entry = enu.nextElement();
				//--
				list.add( entry.getName() );
			}
			zipFile.close();
			
			result = (String[])list.toArray(new String[0]);
		}catch( Exception ex ){
			err = -1;
			Log.d( "Err", "// ZipIO : getList err... //" );
		}

		//--
		return result;
	}

 まず、ZIPのパスから、ZipFile オブジェクトを生成する。

 そして、entries() で、Enumeration を取得する。
 このEnumeration は、中のファイル、──つまりZipEntry の塊みたいなモンだと思っていい。
 Enumeration のnextElement() を使うと、順繰りにZipEntry を取り出せるんだ。

 で、取り出したZipEntry にgetName() をして、ファイル名を取得している。
 取得したファイル名は、いったんList オブジェクトに格納するよ。
 List オブジェクトは配列と似たようなものだけど、要素数を増やすことができるんだ。
 お陰でファイルの総数がわからなくても、配列を作ることができる。
 そして、ファイル名の格納が終わったら、List をtoArray()でString配列に変換するんだ。

 以上で、ファイル一覧を取得できた。
 応用として、getName() をgetSize() にすると、容量一覧が取得できるね。
 ※.getSize() は、long を返します。

 注意点は、ZipFile のclose。
 最後にclose してやらないと、メモリーリークする可能性があるから注意。

文字バケ

 一覧を取得できたので、画面に表示してみた。

文字バケ

 …と。なんかバケてるね。(^_^;
 Windowsで作ったZIPなので、ファイル名がShift-JISなのかもしれない。
 それじゃ、Macで作ったZIPならバケない…?

文字バケ

 …見事にバケてるよ。(w

 これはたぶん、Javaレベルの問題。
 1バイト文字専用で、2バイトやUTF-8に対応していないんだね。
 バケ方のちがいからすると、そんな感じ。

 2バイト文字に対応するには、取得したファイル名をデコードしてやらないといけないんだね。
 まぁ、解凍には支障ないんで、今回はパスしておくよ。

ファイルを解凍してみた

 次に、ファイルを指定して、解凍してみた。

 下記method で、指定したファイルを解凍するよ。
 手順は、ファイルのコピーとそう変わらないね。

 引数は、
  ZIPのパスを指定
  解凍するファイル名
  解凍先(Path付ファイル名)

	public int err = 0;	// エラーがあったか? // -1 : エラー有り | other : エラーなし

	public void meltFile( String zipPath, String file, String outFile ){
		err = 0;
		try{
			ZipFile zipFile = new ZipFile( zipPath );
			ZipEntry entry = zipFile.getEntry( file );
			InputStream is = zipFile.getInputStream( entry );
			
			OutputStream os = new FileOutputStream( outFile );

			byte[] buffer = new byte[ 1024*4 ];	// 1024*4 = DEFAULT BUFFER SIZE
			int r = 0;
			while( -1 != ( r = is.read(buffer)) ){
				os.write( buffer, 0, r );
			}

			os.close();
			is.close();
			zipFile.close();
		}catch( Exception ex ){
			err = -1;
			Log.d( "Err", "// ZipIO : meltFile err... //" );
		}

		//--
	}

 ディレクトリの中にファイルがある場合、解凍するファイル名をPath付にしてやる。
 こんな感じ。

	dir/char.gif

 ただし、2バイトの場合、↓ではダメ。

	新しいフォルダ/char.gif

※.UTF-8 のZIPなら解凍できるかも…?

 たぶん、「1バイト化け」した状態で指定してやれば解凍できるんじゃないかな?
 もしくはファイル名以外の方法で、ZipEntry を取得してやればいいと思う。

 ちなみに上記サンプルでは、解凍先にディレクトリは作られない。
 必要があるなら、別途つけくわえてね。

画像データを取得してみた

 データとして取り出すにはどうしたらいいんだろ?
 ここにひとつ問題があるんだ。

 一覧取得でちょっと触れたけど、ファイルサイズの単位は、long なんだ。
 これではbyte配列自体が作れない。
 int サイズに限定すればできるけど、そういう制限はかけたくない。

 となると、InputStream から直接、目的のデータにすることになりそう。
 というワケで。
 画像データ(Bitmap)として取り出すmethod を作ってみた。

	public int err = 0;	// エラーがあったか? // -1 : エラー有り | other : エラーなし

	public Bitmap meltBitmap( String zipPath, String file ){
		err = 0;
		Bitmap result = null;
		//--

		try{
			ZipFile zipFile = new ZipFile( zipPath );
			ZipEntry entry = zipFile.getEntry( file );
			InputStream is = zipFile.getInputStream( entry );
			result = BitmapFactory.decodeStream( is );

			is.close();
			zipFile.close();
		}catch( Exception ex ){
			err = -1;
			Log.d( "Err", "// ZipIO : meltBitmap err... //" );
		}

		//--
		return result;
	}

 BitmapFactory で、InputStream からBitmap にしている。
 この方法は、ネットから画像データを取得するのと同じ方法。

 Drawable で取り出す時には、

	Drawable.createFromStream

 を使えばいいよ。

 取得した画像データをImageView で表示してみた。
 問題なし。(^_^)
※.画像サイズが大きいと、失敗することがあるらしい。
 詳しいくは…

「ZipFile ~画像サイズが大きいと読み込み失敗」

解凍表示

by the way…

 個人的に困ったのは、RandomAccessFile が使えないことだね。
 ヘッダー位置の取得方法がないから、部分的に読み込むことができない。
 できないワケじゃないけど、イロイロと面倒なんだな、これが。(^_^;
 一度テンポラリーに、ファイルとして解凍するのがいいのかな…。

Posted in File, 読み込み. Tags: , , , , , , , . ZipFile と ZipEntry ~Androidでzipを解凍する はコメントを受け付けていません。 »