20190307のKotlinに関する記事は3件です。

TornadeFXでメインのウインドウを閉じたり閉じたときの処理を実装する

tornadoFXで出てくるメインウィンドウを閉じいたときの処理で詰まったのでメモ

方法

primaryStageのclose()[閉じるとき]やsetOnCloseRequest()またはshowingProperty().addListener()[閉じる処理を書くとき]を呼ぶ

Main.kt
import tornadofx.*
class Main:App(MyView::class)

class MyView :View() {
    override val root = Form()
    val textfield = textfield ("hello world")
    init{
        with(root){
            fieldset{
                textfield
                //閉じるボタン
                button { action { primaryStage.close() } }
            }
        }
        //プログラム上から閉じたらこっちは呼ばれない
        //primaryStage.setOnCloseRequest { e -> println("close")}
        //こっちだとプログラム上で消しても呼ばれる
        primaryStage.showingProperty().addListener {obs,oldValue,newValue->
            if (oldValue == true && newValue == false) {
                println("close")
            }
        }
    }
}

説明

Appのコンストラクタで指定してるのはJavaFXでいうところのStageに乗せるSceneなので、指定してるViewでcloseしても意味がない。Viewが載っているStageにprimaryStageでアクセスできるので、それのclose()を呼べば閉じる。

消したときの処理も同様。ただし、onCloseRequestはプログラム上からcloseしたときは反応しない(ウインドウ上のXボタンを押したときのみ)ので、このページで言われているようにshowingProperty()を使ったほうがいい。

あとがき

javaFXとの対応関係が解れば当たり前の話だったけど、main周りがかなり違って対応関係が解らずかなり詰まった。

参考

https://torutk.hatenablog.jp/entry/20170613/p1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あの素晴らしいlicense-tools-pluginをもう一度

:cooking: はじめに

Qiita初投稿になります。6年程会社でプログラマとして仕事をし、今はフリーランスでAndroidアプリを中心にJava/Kotlinでゴリゴリ開発してます。今回、Androidアプリで使っているライブラリの表示をする必要がありまして、以前採用させていただいたクックパッドさんの license-tools-plugin を使おうとしたのですが、プラグイン自体のアップデートやIDE(Android Studio)の更新もあり、すんなり導入することができませんでした。公式が クックパッド開発者ブログ で解説していますが、導入方法とハマったポイントを少し長いですが共有します。間違いや補足などがあればコメントにて教えてください。

:cooking: 開発環境

  • PC: MacBook Air (11-inch, Early 2014)
  • OS: macOS Mojave 10.14.3
  • IDE: Android Studio 3.3.2

:cooking: license-tools-pluginとは

クックパッドさんが開発したGradleの神プラグインです。プロジェクトで使用しているライブラリのライセンスをYAMLで管理、HTMLもしくはJSONに一覧を出力してくれます。ライセンスの追記漏れもチェックしてくれる優れものです。GitHubのリポジトリは こちら です。

:cooking: 導入

Gradleのプラグインなので、プロジェクトルート直下の build.gradle に追記します。記事執筆時の最新バージョンは 1.7.0 でした。

build.gradle[root]
buildscript {
    ...

    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.cookpad.android.licensetools:license-tools-plugin:1.7.0' // 追加
    }
}

アプリ内で使用する為に、app直下の build.gradle の冒頭に追記をします。

build.gradle[app]
apply plugin: 'com.cookpad.android.licensetools' // 追加

動作する為に JDK8 以上が必須とのことなので、こちらも build.gradle に互換性を追記しておきます。 JDK8 はAndroid Studio 3.3.2であれば、ビルトインとしてあらかじめ用意されているはずです。

build.gradle[app]
android {
    ...

    // 追加
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

全ての追記が終わったら Sync Project を行い、エラーが出ていなければOKです。

:cooking: 使い方

ライブラリの依存関係を確認

ライブラリの依存関係を確認します。今回はデモの為に、Androidアプリの開発でよく使うものを追加しました。

build.gradle[app]
dependencies {
    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    // Android Support Library
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:cardview-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    // Square
    implementation 'com.squareup.moshi:moshi:1.8.0'
    implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'
    implementation 'com.squareup:otto:1.3.8'
    implementation 'com.squareup.picasso:picasso:2.71828'
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'

    // ReactiveX
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.7'
}

ライセンス情報の作成

依存関係の記述が終わったら、Android Studioのサイドバーにある Gradle > [project-name] > :app > Tasks > verification から、 checkLicenses タスクを実行します。コンソールにライセンス情報の一覧が出力されるので、app直下に licenses.yml を作成して内容をコピペします。

licenses.yml
- artifact: android.arch.core:common:+
  name: Android Arch-Common
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.squareup.okhttp3:okhttp:+
  name: OkHttp
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: org.jetbrains.kotlin:kotlin-stdlib:+
  name: org.jetbrains.kotlin:kotlin-stdlib
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://kotlinlang.org/
- artifact: com.squareup.okio:okio:+
  name: Okio
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: com.android.support:support-annotations:+
  name: Android Support Library Annotations
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:design:+
  name: Material Components for Android
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-utils:+
  name: Android Support Library core utils
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:interpolator:+
  name: Android Support Library Interpolators
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: The Apache Software License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: http://developer.android.com/tools/extras/support-library.html

# 以下、長いので省略

ライセンス情報の編集

その後 licenses.yaml の情報を編集していきます。具体的には #COPYRIGHT_HOLDER##LICENSE# と言ったプレースホルダ部分を編集します。例えば

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: com.squareup.retrofit2:retrofit:+
  name: Retrofit
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#
- artifact: io.reactivex.rxjava2:rxjava:+
  name: RxJava
  copyrightHolder: #COPYRIGHT_HOLDER#
  license: #LICENSE#

のような一覧が出力された場合は

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: The Android Open Source Project
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://developer.android.com/topic/libraries/support-library/
- artifact: com.squareup.retrofit2:retrofit:+
  name: Retrofit
  copyrightHolder: Square, Inc.
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://github.com/square/retrofit/
- artifact: io.reactivex.rxjava2:rxjava:+
  name: RxJava
  copyrightHolder: RxJava Contributors
  license: The Apache License, Version 2.0
  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
  url: https://github.com/reactivex/rxjava/

となります。基本的にGitHubの情報を元に調べ編集していきます。Android Open Source Project(AOSP)は ここ にライセンスに関する内容が記載されています。全ての情報の編集が終わったら、再度 checkLicenses タスクを実行し、エラーが発生しなければOKです。

ライセンス情報の表示

最後に Gradle > [project-name] > :app > Tasks > other から、 generateLicensePage タスクを実行し、出力されたHTMLファイルをアプリ内の WebView に読み込ませます。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    webView.loadUrl("file:///android_asset/licenses.html")
}

screen.gif

:cooking: ハマったこと

checkLicensesタスクが失敗する

checkLicenses タスクを実行した際に missing libraries in licenses.yml というエラーで失敗してしまう。初回実行時には licenses.yaml が存在しないので、コンソールに表示されたライセンスの情報一覧をコピペして作成すればいいのですが、2回目以降であれば不足している情報がコンソールに出力されていますので、YAMLファイルに追記します。また、使用しているライブラリが、内部的に別のライブラリに依存している場合(例えば appcompat-v7 は内部的に support-annotationssupport-compat に依存している)、その関係性も監視対象になります。そういったライブラリは

licenses.yaml
- artifact: com.android.support:appcompat-v7:+
  name: Android AppCompat Library v7
  copyrightHolder: The Android Open Source Project
  license: The Apache License, Version 2.0
- artifact: com.android.support:support-annotations:+
  skip: true
- artifact: com.android.support:support-compat:+
  skip: true

と記述することで、一覧から除外することができます。また、グループ一式で除外する場合は

licenses.yaml
- artifact: com.android.support:+:+
  skip: true

の様にワイルドカードで指定したり、

build.gradle[app]
licenseTools {
    ignoredGroups = ['com.android.support']
}

の様に記述することで可能となります。

generateLicensePageタスクが失敗する

コンソールでエラーの内容を確認すると checkLicenses タスクでコケていることがほとんどかと思います。 checkLicensesタスクが失敗する を参考に解決します。

必要な項目が不足している

licenses.yaml に必要な項目が不足している場合にも、プラグインのタスクが失敗します。必須項目は以下の通りです。

  • artifact
  • name
  • copyrightHolder/ author/ authors/ noticeのうちどれか1つ

オプションとして付与できる項目は

  • year
  • skip
  • forceGenerate

などがあります。詳しくは公式の GitHubリポジトリ を参照してください。

:cooking: 最終的な着地点

ここまでのハマった箇所を踏まえて、デモ用のアプリとしてGitHubに demo-licenses をアップしておきます。参考になるかわかりませんが、躓いていらっしゃる方の助けに少しでもなれたらなと思います。表示させないライセンスについてですが、最終的には ignoredGroupsskip を併用することで解決しました。 ignoredGroups は、名前の通り使用しているライブラリの groupId のみにしか対応しておらず artifactId では除外できませんでした。 groupId + artifactId でもプラグインの監視対象から除外できれば良いのになぁ。

:cooking: まとめ

結果的に調べたりなんなりで作業自体のコストはかかってしまいましたが、1から全て自前で実装するよりは遥かに良いです。今後、同様な実装が発生した場合にはこの記事を参考にスピーディに対応したいですね。また、先ほどの groupId + artifactId で除外するPull Requestなどを本家に出してみたいと思っています。ライセンスの表示は、Androidアプリを開発していく上で必要なことではありますが、開発が進むにつれて管理が億劫になったり、時間を割くことが難しいかもしれません。そんな痒いところに手が届くようなライブラリを開発してくださったクックパッド様には感謝しかないです :pray:

:cooking: どうでも良い話

ライセンスの綴りとして licenselicence があります。大きな違いとして、イギリスでは動詞の場合に『license』名詞の場合に『licence』を使い分けますが、アメリカでは特に区別をせず日常的に『license』が使われるみたいです。結構タイプミスするので気をつけたいです。

:cooking: 参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】画像をカルーセル表示するときにハマった話

RecyclerViewを使うと簡単に画像をカルーセル表示することができます。
ですが、ImageViewの扱いでハマった部分があったので、その際に調査した内容をまとめました。

TL;DR

ImageViewのAdjustViewBoundstrueをセットすることで、ImageViewのサイズを画像サイズに合わせて調整することができます。

前提条件

カルーセル、画像のサイズおよび表示方法に関する条件は以下の通りです。

  1. カルーセルの高さは固定とする
  2. カルーセルに表示する各画像のサイズはすべて異なる可能性がある
  3. 各画像の高さはカルーセルの高さと一致するように拡大縮小する
  4. 各画像のアスペクト比は維持する
  5. 各画像はクロップしない

ハマった内容

以前作成したScaleTypeと表示画像の対応表を参考に、ScaleTypeは一旦FIT_CENTER(デフォルト値のためxml上の指定なし)、またlayout_widthwrap_contentlayout_height100dpとしました。

RecyclerViewの各要素に表示する画面のxmlは以下の通りです。

item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/imageView"
  android:layout_width="wrap_content"
  android:layout_height="100dp" />

正直どのScaleTypeも今回の条件を満たさないと感じつつも、layout_widthwrap_contentにすることで、FIT_CENTERであれば画像の高さをカルーセルの高さと一致させ、それに合わせて画像全体が含まれるようにアスペクト比を維持しながら幅を調整してくれるのでは?というわずかな希望を抱きながら試してみました。

結果は以下のようになりました。案の定、希望通りの表示になりませんでした。
before.png
これを見て、画像が小さいときの結果(左)は納得できるのですが、画像が大きいときの結果(右)に関しては何で左右に余白ができるのかが理解できませんでした。

解決方法

冒頭でも触れましたが、ImageViewのAdjustViewBoundstrueにすることで解決できました。これは文字通り「Viewの境界を調整する」ための属性です。
公式ドキュメントはこちらにあります。

AdjustViewBoundstrueにすると、結果は以下のようになりました。上が先ほどの結果(デフォルト値はfalse)で、下が今回の結果です。
after.png
これで、ImageViewのサイズを画像サイズに合わせて調整することができました。

AdjustViewBoundsをセットしたときの内部処理

結果としてはこれで問題ないのですが、画像が大きいときの結果に関してはこれだけでは理解ができなかったので、ImageViewの処理を追ってみました。
ImageView.javaのソースコードはこちらにあります。

Androidにおいて、Viewが表示されるまでの大まかな流れは以下の通りになります。今回の肝となるのは1.のonMeasure()です。

  1. サイズを決める(onMeasure())
  2. 場所を決める(onLayout())
  3. 描画する(onDraw())

これに関してはこちらの記事を参考にさせていただきました。


では、ここからはImageViewのonMeasure()の処理を追っていきます。

最初は各変数を宣言しているだけだったので割愛します。
以下の処理が1つ目のポイントになります。

ImageView.java#LL.1088-1089
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

widthMeasureSpecheightMeasureSpeconMeasure()の引数で、そこからMeasureSpecModeを取得しています。
ただ、widthMeasureSpecheightMeasureSpecがどのように決定されるのかは正直よく分かりませんでした。

MeasureSpecは親Viewから子Viewに対して課される制約を表しており、以下の3種類のModeがあります。公式ドキュメントはこちらにあります。

MeasureSpec.Mode 制約条件
UNSPECIFIED 親Viewによって子Viewのサイズが決定されない
EXACTLY 親Viewによって子Viewの正確なサイズが決定される
AT_MOST 親Viewによって子Viewの最大のサイズが決定される

したがって、widthSpecModeMeasureSpec.UNSPECIFIEDheightSpecModeMeasureSpec.EXACTLYとなります。



そして、以下の処理が2つ目のポイントになります。
AdjustViewBoundstrueの場合のみ以下の処理が行われます。

ImageView.java#LL.1104-1109
if (mAdjustViewBounds) {
  resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
  resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
  desiredAspect = (float) w / (float) h;
}

resizeWidthresizeHeightは幅・高さのリサイズを行うか否かを制御する変数です。
desiredAspectは画像のアスペクト比(wは幅、hは高さ)を表す変数です。

今回の場合、widthSpecModeMeasureSpec.UNSPECIFIEDheightSpecModeMeasureSpec.EXACTLYなので、AdjustViewBoundstrueのときはresizeWidthのみがtrueとなります。



そして、以下の処理が3つ目のポイントになります。
長いので詳細は省略しますが、おおむね以下のような処理が行われています。

  1. AdjustViewBoundstrueのとき
    ImageViewのサイズを画像サイズに合わせて調整する
  2. AdjustViewBoundsfalseのとき
    ImageViewの幅を画像の幅と一致するよう調整する
ImageView.java#LL.1120-1190
int widthSize;
int heightSize;
if (resizeWidth || resizeHeight) {
  // 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);
  if (desiredAspect != 0.0f) {
    // See what our actual aspect ratio is
    final float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom);
    if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
      boolean done = false;
      // 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 && !sCompatAdjustViewBounds) {
          widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
        }
        if (newWidth <= widthSize) {
          widthSize = newWidth;
          done = true;
        }
      }
      // Try adjusting height to be proportional to width
      if (!done && resizeHeight) {
        ...
      }
    }
  }
} else {
  w += pleft + pright;
  h += ptop + pbottom;
  w = Math.max(w, getSuggestedMinimumWidth());
  h = Math.max(h, getSuggestedMinimumHeight());
  widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
  heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);



ここで、else句の最後にあるresolveSizeAndState()MeasureSpec.UNSPECIFIEDのときは引数に指定した画像サイズそのものを返していたので、ImageViewの幅が画像の幅と同じサイズで表示されるということが分かりました。スッキリ。
View.javaのソースコードはこちらにあります。

View#LL.23463-23483
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
  final int specMode = MeasureSpec.getMode(measureSpec);
  final int specSize = MeasureSpec.getSize(measureSpec);
  final int result;
  switch (specMode) {
    case MeasureSpec.AT_MOST:
      if (specSize < size) {
        result = specSize | MEASURED_STATE_TOO_SMALL;
      } else {
        result = size;
      }
      break;
    case MeasureSpec.EXACTLY:
      result = specSize;
      break;
    case MeasureSpec.UNSPECIFIED:
    default:
      result = size;
  }
  return result | (childMeasuredState & MEASURED_STATE_MASK);
}

サンプルアプリ

上記の画像キャプチャを撮影したアプリです。
GitHub - AdjustViewBoundsChecker

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む