ShapeDrawableのRoundRectShapeとGradientDrawableをそれぞれ使って角丸ボタン作ってみた
ShapeDrawableのRoundRectShape、あるいは、GradientDrawableを使うとコードからフチの余白や色を指定できる形で角丸四角形の背景を持つボタンが作ることができます。
もともとShapeDrawable
で書いていたのですが、こちらの記事でShapeDrawable
のリソースをGradientDrawable
にコンパイルされていることを知り、GradientDrawable
で書いてみたらスッキリ記述できたので書き直した、という話です。
どちらにせよ、リソースファイルから読み込んだほうが基本的に高速なので、あまり表示頻度が多くない箇所をカスタマイズして表示したいとか、そういった場合に使えば良いと思います。
ShapeDrawableのRoundRectShapeで作る場合
同じ指定をxmlで記述した場合
今回はxmlで記述した場合は下記のようになる指定をコードで実装することにします。
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <padding android:left="@dimen/button_padding_horizontal" /> <padding android:top="@dimen/button_padding_vertical" /> <padding android:right="@dimen/button_padding_horizontal" /> <padding android:bottom="@dimen/button_padding_vertical" /> <!-- 上記の指定はButton要素に下記属性を追加するのと同じ結果となる。 android:paddingLeft="@dimen/button_padding_horizontal" android:paddingTop="@dimen/button_padding_vertical" android:paddingRight="@dimen/button_padding_horizontal" android:paddingBottom="@dimen/button_padding_vertical" --> <!-- デフォルトのボタンではButton要素は paddingを設定しなくてもButton要素に余白を持っているが、それは android.R.drawable.btn_default_normal.xmlなどで paddingが指定されているため --> <solid android:color="#f00"/> <corners android:radius="@dimen/button_round_radius"/> </shape>
RoundRectShapeを利用してコードで記述した場合
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); ShapeDrawable shape = new ShapeDrawable(); shape.setShape(new RoundRectShape(new float[] {roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius}, null, // 背景のうち、真ん中の方をくりぬいた形にすることができるのですが、その領域の大きさをRectFで指定可能. 利用しない(=一色塗りつぶしにする)場合はnull null)); // 背景のうち、くりぬいた部分の長方形の角丸指定. 利用しない(=くりぬいた部分は長方形のまま)場合はnull // 余白の設定 int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal); int verticalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_vertical); shape.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); shape.getPaint().setColor(Color.RED); if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(shape); } else { button.setBackgroundDrawable(shape); }
<!-- レイアウト --> <Button android:id="@+id/button_bg_resource" android:layout_below="@id/some_element" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/test" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" /> <Button android:id="@+id/button_bg_code" android:layout_below="@id/button_bg_resource" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" />
<!-- dimenリソースの設定 --> <resources> <dimen name="button_round_radius">8dp</dimen> <!-- この辺の元ネタはこちら https://github.com/android/platform_frameworks_base/blob/c29fff50322599f53feadf9cf87df9956c9ac44e/core/res/res/values/dimens_material.xml#L109-L116 --> <dimen name="button_padding_horizontal">8dp</dimen> <dimen name="button_padding_vertical">4dp</dimen> </resources>
見た目はこんな感じです。上がdrawableリソースで背景を指定したボタンで下がコードで背景を指定したボタンになります。
GradientDrawableで作る場合
コードでの記述
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setCornerRadius(roundRadius); gradientDrawable.setColor(Color.RED); // 余白の設定はButton要素の属性でやる if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(gradientDrawable); } else { button.setBackgroundDrawable(gradientDrawable); }
<!-- レイアウト --> <Button android:id="@+id/button_bg_resource" android:layout_below="@id/some_element" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/test" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" /> <Button android:id="@+id/button_bg_code" android:layout_below="@id/button_bg_resource" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="@dimen/button_padding_horizontal" <!-- GradientDrawableの場合はpaddingを設定するメソッドはないので、 View側にpaddingの指定を置く --> android:paddingTop="@dimen/button_padding_vertical" android:paddingRight="@dimen/button_padding_horizontal" android:paddingBottom="@dimen/button_padding_vertical" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" />
見た目は変わってません。
参考
- https://developer.android.com/reference/android/graphics/drawable/ShapeDrawable.html
- http://qiita.com/guchio/items/bebc13db0d2c0ae03363
- https://developer.android.com/guide/topics/resources/drawable-resource.html#Shape
- https://developer.android.com/reference/android/graphics/drawable/GradientDrawable.html
AndroidのNDKやABIについてのメモ
はじめに
C系の言語で動いているクロスプラットフォームのブリッジを作成するには、そのコンパイルなどの過程でNDKに関する知識があるとトラブルシューティングやテストケースの設計に役立つことがあります。
なので、ここではその概要をまとめておきます。はじめに用語からまとめようと思ったのですが、
https://developer.android.com/ndk/guides/index.html?hl=ja
Native Development Kit(NDK)は、Android アプリで C および C++ コードを使用できるようにするツールのセットのことです。NDK を使用して独自のソースコードをビルドしたり、事前にビルドされた既存のライブラリを活用したりできます。
https://developer.android.com/ndk/guides/concepts.html?hl=ja
アプリケーション バイナリ インターフェース(ABI)はアプリのマシンコードが実行時にどのようにシステムと連携するかを正確に定義したものです。 NDK はこれらの定義に対して
.so
ファイルをビルドします。
と言われてもよくわからなかったので、NDKがABIを使ってC, C++のソースコードから.so
ファイルをビルドする周辺で何をしているかを説明します。
.soファイルとは
と書いたものの、書いた本人は.soファイルについての事前知識が調査開始時点でなかったため、簡単に説明します。
so
はshared object file
の略です。
.so
ファイルは、UNIX系の環境下での共通ライブラリファイルのことで、Windowsでいうdllにあたります。具体的には、複数の実行ファイル間で共有して利用される処理の定義がまとめられています。
中身は実行形式ファイルですが、あくまで処理を含んでいるだけでエントリポイントがなく単体で起動することはできません。
各々の実行ファイルに含まれず、実行ファイルを起動した時に動的にロードとリンクが行われるため、動的(ダイナミック)リンクライブラリと呼ばれることがあり、Android Developers GuideのNDKについての記述では主に動的リンクライブラリ、と呼称されています。
Androidでは、アプリのコンパイル時にリンクされている方が.a
(静的リンクライブラリ), アプリの起動時にロードされてリンクされる方が.so
で、NDKを使ってABIに従って作成される方が基本的に.so
という感じです。
自分が作成したライブラリが.so
か、.a
になるかはコンパイルの設定によりますが、Androidアプリのビルドの文脈だと、.so
ファイルの方しか、公式のドキュメントに記載がなかったので、こちらが多いかもしれません。
NDKを利用してビルドする際の流れ
https://developer.android.com/ndk/guides/concepts.html のフローをもとに適宜用語の説明を入れながら説明します。
- Androidプロジェクトを作成します
- Androidプロジェクト直下にjniディレクトリを作成し、ネイティブライブラリ(.aファイル, .soファイルなど.アーキテクチャごとにフォルダを用意したり…)とコンパイル対象のソースコード(CやC言語で書かれたソースコード)とそれらをコンパイルするモジュールにどう含むか記述していくAndroid.mkを配置
- 2のディレクトリに任意でターゲットABI, ツールチェーン, リリース/デバッグモード, STLを設定するApplication.mkファイルを作成(任意)
- デフォルト設定
- ABI: armeabi
- ツールチェーン: GCC 4.8(4.8 32bit / 4.9 64bit端末対応らしい)
- モード: リリース
- リリースモードだと変数宣言が最適化されていたりして、C言語側のステップ実行が困難になったりします
- STL: system
- NDKツールを適切に利用するために作成されたシェルのラッパーであるndk-buildを使用して、Cのソースコードをネイティブ(.so, .a. 主に.so?)ライブラリへコンパイルしたり、静的ライブラリをリンクしたり
- Javaのソースコードをビルドして、DVMで実行可能な.dexファイルを生成
- アプリの実行に必要な .so, .dex、その他ファイルをすべてapkファイルにパッケージ化
ところで
.soファイルやネイティブライブラリと.dexを一緒に処理していませんが、JNI用のヘッダとそれに対応した規則のメソッド名で作成していけば、JVM(DVM)上でメソッドテーブルが作成されると理解していて、それにしたがってJava<=>C,C++間でメソッドを利用し合うことが可能みたいです。
この辺を調べていると、CMakeといった言葉もでてきますが、ひとまずこの記事を書くにあたっての状況に限っては(Andorid Studioでビルドをしない場合は)関係なかったので省きます。
CMakeについては、また今度機会あればまとめます。
参考
https://developer.android.com/ndk/guides/concepts.html http://blog.katty.in/4347 http://www.peliphilo.net/archives/681 http://qiita.com/m1takahashi/items/3a3c9d2845e9b57aeda3 http://morado106.blog106.fc2.com/blog-entry-80.html
emulator: ERROR: This AVD's configuration is missing a kernel file!
エミュレータネタ連発。上記エラーが発生した時に確認することを順にメモ。
エラーメッセージ全体
Cannot launch AVD in emulator. Output: emulator: ERROR: This AVD's configuration is missing a kernel file! Please ensure the file "kernal-qemu" is in the same location as your system image. emulator: ERROR: ANDROID_SDK_ROOT is defined (/Users/woshidan/Library/Android/sdk) but cannot find kernel file in (/Users/woshidan/Library/Android/sdk/system-images/) sub directories
発生したエミュレータのAPIバージョンとABIとどこ製か
一部のエミュ(主にARM系)のエミュにはGoogle社製のものとAndroid Open Source Projectによるものがあって、自分の手元で発生したのは
で発生。
kernal-qemuとは
QEMU(仮想マシンエミュレータ)のハード的なところとソフト的なところをつないでくれるソフト。カーネル。 どうも、Androidのエミュレータは、QEMUを利用してARM系のエミュをx86などのCPUのPC上で動かしているそうです。
今回のエラーはエミュレータ作成の時に指定したターゲットに対して、このファイルがないと言われています。
実はでもなく、システムイメージが何かよくわかってないので用語や文末がふわふわですが、エミュレータに指定できるターゲットに相当するディレクトリがありまして、そのディレクトリの中に kernal-qemu
がないと言われていることになります。
発生した場合に確認すること
- SDK Manager上で該当システムイメージがインストールされているか
- 一度エミュレータを作成した後にシステムイメージが壊れていたり、Android StudioのAndroid SDKのパスの設定がおかしくて認識されてなかったりするかも?
- システムイメージが入っているディレクトリを開いてみて
kernal-qemu
があるか
実際に当該ディレクトリを覗いてみた例
// エラーが出ない $ ls /Users/woshidan/Library/Android/sdk/system-images/android-15/google_apis/x86 NOTICE.txt kernel-ranchu source.properties build.prop package.xml system.img kernel-qemu ramdisk.img userdata.img // エラーが出る $ ls /Users/woshidan/Library/Android/sdk/add-ons/addon-google_apis-google-16-1/images/armeabi-v7a NOTICE.txt build.prop ramdisk.img system.img userdata.img
どうするか
本来はないと言われているkernel-qemu
を調達できればいいのですが、いまのところ、Linuxよくわかってないし、方法不明*1。
もしかすると、SDK Managerをアップデートした後、システムイメージをアンインストールして再インストールすると直るかもしれないですが、最新バージョンのシステムイメージが欲しい場合などを除き、Android Open Source Projectの方のディレクトリには kernel-qemu
が含まれていたため、未検証です。
また、自分がこの不具合に遭遇したARM系というのはたいていの実機が該当するので、実機で検証するか実機が欲しいと交渉した方が早いかもしれません。
現場からは以上です。
*1:誰かわかったら教えてください
ERROR: resizing partition e2fsck failed with exit code 8
Androidのエミュレータの設定をいじっている最中、上図のエラーダイアローグを稀によく見かけていたような気がするのですがいままで原因がわからないうちになんか回避してしまっていたのでした。その原因と対応がなんとなくわかったのでメモ。
Androidのエミュレータの設定やデータは /Users/${username}/.android/avd
ディレクトリの中にそれぞれのエミュレータ専用の設定ファイル(.ini
)と設定ファイルやデータをしまうためのディレクトリ(.avd
)が用意されています。
avd$ pwd /Users/woshidan/.android/avd avd$ ls 5.1_WVGA_API_21.avd 5.1_WVGA_API_21.ini API15.avd API15.ini ...
個々のエミュレータ用の設定やデータが入っている.avd
の中身は以下となっています。
API15.avd$ ls cache.img hardware-qemu.ini userdata.img config.ini sdcard.img emulator-user.ini userdata-qemu.img
件のエラーはこの中のデータなどが入っているファイルシステムである .img
ファイルの形式がおかしいため起こるようです。これは、エミュレータの設定をいじってから再起動などをしていると起こることがあります。
エラーメッセージ中にあるe2fsck
コマンドについてはこちらによると、
ext2/ext3/ext4ファイルシステムの整合性をチェックし、修復する。マウント中のファイルシステムに対してはこのコマンドを実行しないようにする(修復をかけるとファイルシステムが壊れる恐れがある)。
なので、このコマンドがうまく走ってくれたらファイル形式の問題は解決しそうです。
また、一部の .img
ファイルは起動時に存在しなければ作り直されるので、中に入っているデータに未練がなければ消して作り直してみてもよさそうです。エミュだし。ということで、
- 手動で
e2fsck
を各ファイルにかけてみる cache.img
,userdata-qemu.img
を削除する- エミュレータを作り直す
などで解決しそうですというか、私は2つ目と3つ目で解決した後StackOverflowで一つ目の方法を見つけたのでした。
現場からは以上です。
後からコード上でBitmapを与える場合、ImageViewのadjustViewBounds=trueの指定が動かない
結論
onMeasureで一部人間にはよく分からない動きをしている気がします。
- ImageViewの
android:adjustViewBounds
属性がtrueで、ImageViewの幅などを直接指定していない場合- ImageViewを許される限り拡大した場合の高さと幅を求める(widthSize/heightSize)
- 上記の高さ or 幅を適用したとして、他辺の長さを求める(newWidth/newHeight= widthSize * 縦横比の場合 / heightSize / 縦横比)*1
- 下記の最小値 >=newWidth/newHeightの場合、ImageViewのリサイズが行われる
- DrawableのサイズとPaddingの合計値(in pixel)
- 親Viewから許容されている値(in pixel)
android:minHeight
あるいはandroid:minWidth
で指定している値(in pixel)
- 上記の最小値 < newWidth/newHeightの場合、ImageViewのリサイズが行われない
- 現状のImageViewとDrawableのアスペクト比がほぼ同じ場合もonMeasureでリサイズしない
コード読み
主に関係あるのはImageViewのonMeasureメソッドなのでこれを読んでいきます。
また、下記のケースは親Viewから具体的にImageViewの大きさが制限されていない*2場合のケースとなります。
android:adjustViewBounds="true"
の場合、まずDrawableのサイズを取得します。
w = mDrawableWidth; h = mDrawableHeight; if (w <= 0) w = 1; if (h <= 0) h = 1; // We are supposed to adjust view bounds to match the aspect // ratio of our drawable. See if that is possible. if (mAdjustViewBounds) { resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; desiredAspect = (float) w / (float) h; }
そのあとにPadding
の値を取得します。
int pleft = mPaddingLeft; int pright = mPaddingRight; int ptop = mPaddingTop; int pbottom = mPaddingBottom;
そして、現在の条件でImageViewを拡大していい最大サイズの計算をします。
if (resizeWidth || resizeHeight) { /* If we get here, it means we want to resize to match the drawables aspect ratio, and we have the freedom to change at least one dimension. */ // Get the max possible width given our constraints widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec); // Get the max possible height given our constraints heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
次に、縦か横をこのどちらかの大きさにあわせた場合のもう一辺の長さを計算します。
// 横方向にリサイズしていい場合は、まず縦の長さを目一杯撮った場合の横幅(newWidth)の計算をします // Try adjusting width to be proportional to height if (resizeWidth) { int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright; // Allow the width to outgrow its original estimate if height is fixed. if (!resizeHeight && !mAdjustViewBoundsCompat) { widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec); }
新しく計算した長さが、先ほど計算したImageViewを拡大していい最大サイズ以下であればリサイズをします。
if (newWidth <= widthSize) { widthSize = newWidth; done = true; }
横方向にリサイズできなかった場合は、縦方向のリサイズを試みます。
resolveAdjustedSize
の中身ですが、MeasureSpec
の値が、MeasureSpec. AT_MOST
の場合、
- DrawableのサイズとPaddingの合計値(in pixel)
- 親Viewから許容されている値(in pixel)
android:minHeight
あるいはandroid:minWidth
で指定している値(in pixel)
の最小値となっています。
private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) { int result = desiredSize; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: /* Parent says we can be as big as we want. Just don't be larger than max size imposed on ourselves. */ result = Math.min(desiredSize, maxSize); break; case MeasureSpec.AT_MOST: // Parent says we can be as big as we want, up to specSize. // Don't be larger than specSize, and don't be larger than // the max size imposed on ourselves. result = Math.min(Math.min(desiredSize, specSize), maxSize); break; case MeasureSpec.EXACTLY: // No choice. Do what we are told. result = specSize; break; } return result; }
現場からは以上です。
*1:少し大雑把なので、正確にはこの辺 https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1010
*2:親Viewのandroid:layout_widthがwrap_contentなどで、MeasureSpec.getModeでMeasureSpec.AT_MOSTが返ってくる場合
AndroidのImageViewのscaleTypeについて(コード読んだメモ編)
前の記事の検証する前に、いまいち動作がはっきりしないところがあったのでコード読んでいたときのメモです。
力尽きたので雑なのですが。。
どこでscaleTypeの設定を適用しているかを調べるために、 https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java をscaleTypeなどで検索するところから始めました。
読んでいるソースコードがMarshmallowのものである理由は、1/20時点でダッシュボードにて確認したら一番シェアが高かったためです。
目次
- 関連しそうなソースコード
- configureBounds()内で処理されるscaleTypeについて
- mDrawMatrixがどこで使われるかについて
- scaleTypeごとのコードリーディング
- scaleType: CENTER
- scaleType: CENTER_CROP
- scaleType: CENTER_INSIDE
- scaleType: FIT_XY
- scaleType: MATRIX
- FIT_CENTER, FIT_END, FIT_START
- ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない
関連しそうなソースコード
さて、初期化以外でscaleTypeが出てくるのは下記です。
// ImageView.java private void configureBounds() { if (mDrawable == null || !mHaveFrame) { return; } int dwidth = mDrawableWidth; int dheight = mDrawableHeight; int vwidth = getWidth() - mPaddingLeft - mPaddingRight; int vheight = getHeight() - mPaddingTop - mPaddingBottom; boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight); if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { /* If the drawable has no intrinsic size, or we're told to scaletofit, then we just fill our entire view. */ mDrawable.setBounds(0, 0, vwidth, vheight); mDrawMatrix = null; } else { // We need to do the scaling ourself, so have the drawable // use its native size. mDrawable.setBounds(0, 0, dwidth, dheight); if (ScaleType.MATRIX == mScaleType) { // Use the specified matrix as-is. if (mMatrix.isIdentity()) { mDrawMatrix = null; } else { mDrawMatrix = mMatrix; } } else if (fits) { // The bitmap fits exactly, no transform needed. mDrawMatrix = null; } else if (ScaleType.CENTER == mScaleType) { // Center bitmap in view, no scaling. mDrawMatrix = mMatrix; mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f)); } else if (ScaleType.CENTER_CROP == mScaleType) { mDrawMatrix = mMatrix; float scale; float dx = 0, dy = 0; if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } else if (ScaleType.CENTER_INSIDE == mScaleType) { mDrawMatrix = mMatrix; float scale; float dx; float dy; if (dwidth <= vwidth && dheight <= vheight) { scale = 1.0f; } else { scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } dx = Math.round((vwidth - dwidth * scale) * 0.5f); dy = Math.round((vheight - dheight * scale) * 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); } else { // Generate the required transform. mTempSrc.set(0, 0, dwidth, dheight); mTempDst.set(0, 0, vwidth, vheight); mDrawMatrix = mMatrix; mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); } } }
configureBounds()内で処理されるscaleTypeについて
全部のscaleTypeがこのメソッドの中の分岐に登場しているわけではなかったので、登場してるかなどざっくり分けると、
scaleType | configureBounds()内での扱い |
---|---|
CENTER, CENTER_CROP, CENTER_INSIDE, MATRIX, FIT_XY (+ 画像サイズが取得できなかった場合) | 処理あり |
FIT_START, FIT_END, FIT_CENTER | 個別に処理なし |
となっていて、だいたいMatrix.ScaleToFitと同じ内容の処理はここに書いていない、という感じでした。
mDrawMatrixがどこで使われるかについて
ここから、各scaleTypeの処理について、さっきのconfiguraBounds()メソッドの分岐の中を覗いてみて、mDrawMatrixの値がどう設定されているかを見ていくことで、scaleTypeごとにどんな処理がされているかを見ていこうと思います。
といきなり書きましたが、mDrawMatrixの値がどういう風にImageViewの中のDrawableの見え方に影響するかちょっとわかりにくいですね。
このmDrawMatrixがどこで使われているかというと、主にImageView
のonDraw
メソッド内部で実行されるCanvas.concat(Matrix matrix)
メソッドで、Drawable
が持ってるCanvas
のインスタンスに対して拡大縮小などの処理をしています。(細かいところは拡大縮小じゃなくて画像のメモリ管理の話になるので適当にはしょってます)
ImageView onDraw 全体: https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1214-L1247
// 右下方向の余白のために切り抜く if (mCropToPadding) { final int scrollX = mScrollX; final int scrollY = mScrollY; canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, scrollX + mRight - mLeft - mPaddingRight, scrollY + mBottom - mTop - mPaddingBottom); } // 余白の分だけ左上からずらす canvas.translate(mPaddingLeft, mPaddingTop); // Matrixの設定があったら、上記の余白の適用前に、 // configureBoundsで設定したMatrixの設定を適用させる if (mDrawMatrix != null) { canvas.concat(mDrawMatrix); } // DrawableにMatrixなどが適用されたCanvasの中身を書き込み mDrawable.draw(canvas);
scaleTypeごとのコードリーディング
scaleType: CENTER
vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel)
,
mDrawableWidth, mDrawableWidth
(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))
// Drawableの大きさを元の画像の大きさにセットしておく mDrawable.setBounds(0, 0, dwidth, dheight); ... // Center bitmap in view, no scaling. mDrawMatrix = mMatrix; mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f));
https://developer.android.com/reference/android/graphics/Matrix.html#setTranslate(float, float)
Matrix.setTranslate
Set the matrix to translate by (dx, dy).
とあるので、
説明文から見ても、Drawble
の画像の大きさを変えずに画像を中央に移動させています。
scaleType: CENTER_CROP
vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel)
,
mDrawableWidth, mDrawableWidth
(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))
mDrawable.setBounds(0, 0, dwidth, dheight); mDrawMatrix = mMatrix; float scale; float dx = 0, dy = 0; if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
dwidth * vheight > vwidth * dheight
がよくわからないんですけど、
d -> Orig(元画像), v -> Space(画像が入るスペース)と一旦添え字を変えて、
OrigWidth / OrigHeight > SpaceWidth / SpaceHeight
と書き換えると、
アスペクト比を見て、元画像の方が画像が入るスペースより横長の場合は、となるので、
if (元画像の方が画像が入るスペースよりアスペクト比が横長の場合) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; }
となって、scaleについては、元画像の方が画像が入るスペースよりアスペクト比が横長の場合(cf. 縦長のスペースに横長の画像を突っ込んだとか)は、高さを合わせるようにscaleを求めて、その大きさに拡大した画像が横方向に中心に来るように移動する距離(dx)を求めています。
一言で言うと、画像側の短い方の辺の長さを画像が入るスペースに揃えるように拡大縮小したら、はみ出した分を切り取る(ようにcanvas上で画像の位置を移動させている)となります。
scaleType: CENTER_INSIDE
vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel)
,
mDrawableWidth, mDrawableWidth
(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))
mDrawable.setBounds(0, 0, dwidth, dheight); mDrawMatrix = mMatrix; float scale; float dx; float dy; if (dwidth <= vwidth && dheight <= vheight) { // 画像の縦横が、両方画像を入れるスペースの縦横より小さかったら縮小しない scale = 1.0f; } else { // 画像の縦横が、画像入れるスペースの縦横と比べて大きい方に合わせて拡大縮小する // cf. 縦長のスペースに横長の画像を入れるとしたら、画像の横幅が縦長のスペースに合うように拡大縮小する scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } // このとき、なるべく画像がスペースの中心に来るように移動させる dx = Math.round((vwidth - dwidth * scale) * 0.5f); dy = Math.round((vheight - dheight * scale) * 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy);
scaleType: FIT_XY
vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel)
,
mDrawableWidth, mDrawableWidth
(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))
余白分あけてスペースいっぱい引伸ばす。アスペクト比なんてなかった。元画像のサイズがうまく取得できなかった場合も余白分あけてスペースいっぱい引伸ばす。
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { /* If the drawable has no intrinsic size, or we're told to scaletofit, then we just fill our entire view. */ mDrawable.setBounds(0, 0, vwidth, vheight); // onDrawで再度拡大縮小はしない mDrawMatrix = null; }
scaleType: MATRIX
mDrawable.setBounds(0, 0, dwidth, dheight); // Use the specified matrix as-is. if (mMatrix.isIdentity()) { // 単位行列が設定されている場合は、余白などの設定以外何も差せない // (scaleも移動もしない) mDrawMatrix = null; } else { // 設定されているmatrixを適用 mDrawMatrix = mMatrix; }
FIT_CENTER, FIT_END, FIT_START
// Avoid allocations... private RectF mTempSrc = new RectF(); private RectF mTempDst = new RectF(); mDrawable.setBounds(0, 0, dwidth, dheight); // Generate the required transform. mTempSrc.set(0, 0, dwidth, dheight); mTempDst.set(0, 0, vwidth, vheight); mDrawMatrix = mMatrix; // scaleTypeToScaleToFitでScaleTypeをMatrixの方のScaleTypeに変換して、 // Matrix.setRectToRectでそのScaleTypeの効果を適用させるようにしている mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
ここにあるコードブロックではそうなんですが、適用されている効果が何なのかわからないのでもう少し追います。(Cだったらやめます)
private static native void native_setScale(long native_object, float sx, float sy, float px, float py);
Cだったので止めます。
ドキュメントから引用。
Type | Value | description |
---|---|---|
ImageView.ScaleType | FIT_CENTER | Scale the image using CENTER. |
Matrix.ScaleToFit | CENTER | Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst. |
ImageView.ScaleType | FIT_START | Scale the image using START. |
Matrix.ScaleToFit | START | Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst. |
ImageView.ScaleType | FIT_END | Scale the image using END. |
Matrix.ScaleToFit | END | Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst. |
Matrix.ScaleToFit.CENTER
, Matrix.ScaleToFit.START
, Matrix.ScaleToFit.END
の3つ、説明文が全く同じ気がしますが、
Matrix.ScaleToFit.CENTER
中央寄せMatrix.ScaleToFit.START
文頭寄せMatrix.ScaleToFit.END
文末寄せ
の違いがあります。詳しくはこちら。
ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない
boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight); // 中略 } else if (fits) { // The bitmap fits exactly, no transform needed. mDrawMatrix = null;
AndroidのImageViewのscaleTypeについて(検証結果スクショ編)
AndroidのImageViewのscaleTypeについて試したのでまとめました。
検証結果について
検証用画像
検証結果スクショ
次の記事であるコード読んだときのメモのところで軽く触れているのですが、各種ScaleTypeはサイズがぴったり合っていたら何もしません。そのため、挙動を見る為には、
- 横長のImageViewに縦長の画像(画像の縦幅がImageViewの縦幅より大きい/小さい)
- 縦長のImageViewに横長の画像(画像の横幅がImageViewの横幅より大きい/小さい)
- ImageViewと画像の縦横比は同じだけどサイズが大きい/小さい
といった条件の画像とImageViewを用意してみるとよさそうです、ということで用意して突っ込んだ結果が下表です。
ScaleType | 縦長の画像 in 横長のImageView | 横長の画像 in 縦長のImageView | 正方形 in 正方形 |
---|---|---|---|
指定なし(FIT_CENTER) | |||
CENTER | |||
CENTER_CROP | |||
CENTER_INSIDE | |||
FIT_START | |||
FIT_END | |||
FIT_XY |
検証用レイアウト
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.woshidan.imageapplication.MainActivity"> <!-- scaleTypeを指定して色々試す --> <ImageView android:id="@+id/landscape_small_space" android:src="@drawable/potrait_sample" android:layout_width="100dp" android:layout_height="50dp" android:background="#333" /> <ImageView android:id="@+id/landscape_large_space" android:src="@drawable/potrait_sample" android:layout_width="300dp" android:layout_height="150dp" android:background="#333" /> <!--<ImageView--> <!--android:id="@+id/portrait_small_space"--> <!--android:src="@drawable/landscape_sample"--> <!--android:layout_width="50dp"--> <!--android:layout_height="100dp"--> <!--android:background="#333"--> <!--/>--> <!--<ImageView--> <!--android:id="@+id/portrait_large_space"--> <!--android:src="@drawable/landscape_sample"--> <!--android:layout_width="150dp"--> <!--android:layout_height="300dp"--> <!--android:background="#333" />--> <!----> <!--<ImageView--> <!--android:id="@+id/square_small_space"--> <!--android:src="@drawable/square_sample"--> <!--android:layout_width="50dp"--> <!--android:layout_height="50dp"--> <!--android:background="#333"--> <!--/>--> <!--<ImageView--> <!--android:id="@+id/square_large_space"--> <!--android:src="@drawable/square_sample"--> <!--android:layout_width="300dp"--> <!--android:layout_height="300dp"--> <!--android:background="#333" />--> <!----> </LinearLayout>