woshidan's blog

あいとゆうきとITと、とっておきの話。

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リソースで背景を指定したボタンで下がコードで背景を指定したボタンになります。

f:id:woshidan:20170509141454p:plain

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" />

見た目は変わってません。

参考

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ファイルについての事前知識が調査開始時点でなかったため、簡単に説明します。

soshared 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 のフローをもとに適宜用語の説明を入れながら説明します。

  1. Androidプロジェクトを作成します
  2. Androidプロジェクト直下にjniディレクトリを作成し、ネイティブライブラリ(.aファイル, .soファイルなど.アーキテクチャごとにフォルダを用意したり…)とコンパイル対象のソースコード(CやC言語で書かれたソースコード)とそれらをコンパイルするモジュールにどう含むか記述していくAndroid.mkを配置
  3. 2のディレクトリに任意でターゲットABI, ツールチェーン, リリース/デバッグモード, STLを設定するApplication.mkファイルを作成(任意)
  4. デフォルト設定
  5. ABI: armeabi
    • ABIというのは、ネイティブライブラリをコンパイルする際、コンパイルした.so.aファイルがインストール対象の端末で動くことを保証するための規約みたいなものです。
  6. ツールチェーン: GCC 4.8(4.8 32bit / 4.9 64bit端末対応らしい)
  7. モード: リリース
    • リリースモードだと変数宣言が最適化されていたりして、C言語側のステップ実行が困難になったりします
  8. STL: system
  9. NDKツールを適切に利用するために作成されたシェルのラッパーであるndk-buildを使用して、Cのソースコードをネイティブ(.so, .a. 主に.so?)ライブラリへコンパイルしたり、静的ライブラリをリンクしたり
  10. Javaソースコードをビルドして、DVMで実行可能な.dexファイルを生成
  11. アプリの実行に必要な .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とは - OSS用語 Weblio辞書

QEMU(仮想マシンエミュレータ)のハード的なところとソフト的なところをつないでくれるソフト。カーネル。 どうも、Androidエミュレータは、QEMUを利用してARM系のエミュをx86などのCPUのPC上で動かしているそうです。

今回のエラーはエミュレータ作成の時に指定したターゲットに対して、このファイルがないと言われています。

実はでもなく、システムイメージが何かよくわかってないので用語や文末がふわふわですが、エミュレータに指定できるターゲットに相当するディレクトリがありまして、そのディレクトリの中に kernal-qemu がないと言われていることになります。

発生した場合に確認すること

  • SDK Manager上で該当システムイメージがインストールされているか
  • システムイメージが入っているディレクトリを開いてみて kernal-qemuがあるか
    • ディレクトリのパスの例
      • Google社製 /Users/woshidan/Library/Android/sdk/add-ons/addon-google_apis-google-16-1/images/armeabi-v7a
      • Android Open Source Project製 /Users/woshidan/Library/Android/sdk/system-images/android-15/google_apis/x86

実際に当該ディレクトリを覗いてみた例

// エラーが出ない
$ 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

stackoverflow.com

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が出てくるのは下記です。

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1093-L1176

// 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がどこで使われているかというと、主にImageViewonDrawメソッド内部で実行される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

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L107-L109

// 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だったらやめます)

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/graphics/java/android/graphics/Matrix.java#L848

    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について試したのでまとめました。

検証結果について

検証用画像

landscape_sample potrait_sample square_sample

検証結果スクショ

次の記事であるコード読んだときのメモのところで軽く触れているのですが、各種ScaleTypeはサイズがぴったり合っていたら何もしません。そのため、挙動を見る為には、

  • 横長のImageViewに縦長の画像(画像の縦幅がImageViewの縦幅より大きい/小さい)
  • 縦長のImageViewに横長の画像(画像の横幅がImageViewの横幅より大きい/小さい)
  • ImageViewと画像の縦横比は同じだけどサイズが大きい/小さい

といった条件の画像とImageViewを用意してみるとよさそうです、ということで用意して突っ込んだ結果が下表です。

ScaleType 縦長の画像 in 横長のImageView 横長の画像 in 縦長のImageView 正方形 in 正方形
指定なし(FIT_CENTER) 2017-01-19 2 14 21 2017-01-19 2 15 13 2017-01-19 2 15 45
CENTER 2017-01-19 2 17 41 2017-01-19 2 18 08 2017-01-19 2 18 25
CENTER_CROP 2017-01-19 2 19 49 2017-01-19 2 20 14 2017-01-19 2 20 36
CENTER_INSIDE 2017-01-19 15 54 18 2017-01-19 15 54 25 2017-01-19 15 54 38
FIT_START 2017-01-19 15 58 16 2017-01-19 15 58 24 2017-01-19 15 58 32
FIT_END 2017-01-19 15 56 21 2017-01-19 15 56 31 2017-01-19 15 56 38
FIT_XY 2017-01-19 16 01 31 2017-01-19 16 02 11 2017-01-19 16 02 16

検証用レイアウト

<?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>