20190506のKotlinに関する記事は5件です。

【Android】外部から取得した画像をデコードする時に気をつけること

APIなどを叩いて外部から取得した画像データを利用する場合、エラー等のデコードできない可能性や大きすぎる画像である可能性などを考慮する必要がある。
かなり大きな画像をデコードしてしまうと、Out of Memoryでクラッシュする可能性もあるため、直接ピクセルデータをデコードするのではなく、最初にサイズ情報のみをデコードし、必要なサンプリング数を設定してからデコードすることが必要になる。
こうしたことを考慮すると、以下のようなメソッドをutilメソッドとして定義しておき、使い回すのが好ましい。

    fun decodeBitmapFromByteArray(
        data: ByteArray,
        @Dimension requestWidth: Int,
        @Dimension requestHeight: Int
    ): Bitmap {
        val options = BitmapFactory.Options()

        options.inJustDecodeBounds = true
        BitmapFactory.decodeByteArray(data, 0, data.size, options)

        options.inSampleSize = sampleSize(
            options.outWidth, options.outHeight, requestWidth, requestHeight
        )
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeByteArray(data, 0, data.size, options)
    }

    private fun sampleSize(
        sourceWidth: Int, sourceHeight: Int,
        requestWidth: Int, requestHeight: Int
    ): Int {
        if (requestWidth <= 0 || requestHeight <= 0) {
            return 1
        }
        if (sourceWidth * requestHeight < requestWidth * sourceHeight) {
            return if (sourceWidth > requestWidth) sourceWidth / requestWidth else 1
        }
        return if (sourceHeight > requestHeight) sourceHeight / requestHeight else 1
    }

inJustDecodeBoundsをtrueにしてdecodeByteArrayを呼び出すと、画像をメモリに展開せずに画像サイズを取得することができる。これを利用して画像サイズからサンプリングのサイズを決定し、optionsのinSampleSizeに設定した後に、inJustDecodeBoundsをfalseにして実際にdecodeを行う。

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

Canvasを使ってあるBitmapから正円のBitmapを作成する処理を書いた話

やりたいこと

ある画像から、サムネイルのアイコンを作って表示する処理が必要になったので、Canvasを使って実装してみました。

スクリーンショット 2019-05-06 18.38.46.png
例えばこういうネクタイの画像があったとすると、欲しいのは以下のようなアイコンになります。
スクリーンショット 2019-05-06 18.39.05.png
拡大縮小はせず、中心部分をうまく丸く切り取る処理をする場合、以下のような実装になります。

実際のソースコード

    private fun getCroppedBitmap(bitmap: Bitmap): Bitmap {
        val size = Math.min(bitmap.width, bitmap.height)
        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(output)

        val paint = Paint()

        paint.isAntiAlias = true
        canvas.drawARGB(0, 0, 0, 0)
        canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        val max = Math.max(bitmap.width, bitmap.height)
        val start = (size - max) / 2
        val left = if (bitmap.width < bitmap.height) 0 else start
        val top = if (bitmap.height < bitmap.width) 0 else start
        val rect = Rect(left, top, bitmap.width + left, bitmap.height + top)
        canvas.drawBitmap(bitmap, Rect(0, 0, bitmap.width, bitmap.height), rect, paint)
        return output
    }

解説

やっていることはそんなに難しくはありません。
まず、出力先のBitmapをsize指定で作成します。この時、sizeは元画像の幅/高さのうち小さい方を指定します。
次にCanvasとPaintを用意します。
今回は円形に画像を切り抜くので、Paintにはジャギーが目立たなくなるようにisAntiAliasをtrueに設定します(詳細はアンチエイリアスを参照のこと)。
canvasは透明で塗りつぶしてから、drawCircle(中心点のx座標,中心点のy座標,半径,paint)でsizeの半分の半径でちょうど中央に円を描きます。
paint.xfermodeを設定する前のdrawCircleがDST、その後のdrawBitmapがSRCなので、画像が丸の中に入るようにするにはPorterDuffXfermode(PorterDuff.Mode.SRC_IN)paint.xfermodeに設定します。
最後に切り抜くrectを決めてdrawBitmapすることで、描画処理は完了です。

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

KotlinMPPでAndroid/iOS両対応のHTTPクライアントライブラリを作る

はじめに 

 Android/iOS、両プラットフォーム上で同じようなアプリを開発する際、明らかに同じような処理をKotlin/Swiftで実装することがあり、共通化したいなと思うところがありました。
例えば、通信部分やEntity、ValueObjectなど、OSが違っても共通になる処理については尚更です。こういった部分を二度も違う言語で実装するのは明らかに二度手間で余計な工数がかかりますし、バグが発生する可能性も増します。
なんとかこういった部分を共通化し、一度コードを書けば両デバイスで動かせる、それによって工数の削減をしたい。そういったモチベーションがあったため、KotlinMPPについて調査を行い、実際にそれを用いて両プラットフォームに一度で対応できるHTTPクライアントを作ってみました。

KotlinMPPとは

正式名称 Kotlin Multiplatform Project
Kotlin1.2及び1.3で実験的に提供されている機能です。Gradleでターゲットとなるプラットフォームにどんな方法でビルドをするかを記述することで、一つのKotlinのソースを各プラットフォーム向けに動作する形でビルドすることが出来ます。
例えば、AndroidではJVMで動作するようにビルドする、iOSではネイティブで動作するようにビルドする、といった次第です。
Android,iOSはもちろん、mac,Windows,JSをターゲットとすることが出来ます。

KotlinMPPを使ったHTTPクライアントライブラリ

サンプルプロジェクトを作ってみました。
5hyn3/KotlinMPPHttpClientSample

以下のような言語やビルドツール、ライブラリを組み合わせて作られています

  • kotlin

    • 1.3.31
  • gradle

    • 5.4.1
  • ktor-client

    • 1.1.5
  • kotlinx.serialization

    • 0.11.0
  • kotlinx.coroutin

    • 1.2.1

テストが一つ記述されており、実際にAPIからjsonを取得し、パースしてkotlinのオブジェクトとして取得する部分までの動作を確認することが出来ます。実際にアプリに組み込んで動作を確認したい場合は以下を参考にしてください。

Multiplatform Project: iOS and Android

ビルド成果物としてjarとframeworkができるので、それらを扱う形になります。

現状、AndroidとiOSのみをターゲットに作っていますが、gradleに手を入れることでJSなどにも対応させることが出来ます。

苦労した点

以下サンプルプロジェクトを作った際に遭遇した大変だった点を書いていきます

バージョンごとにビルド時の挙動が違っており適切なバージョンの組み合わせを見つけるのが大変だった

主にGradleによるものです。色々なプロジェクトを参考にしてサンプルプロジェクトを作りましたが、バージョンがちょっとでも違うと全くビルドすら出来ませんでした。また、ライブラリが要求するGradleのバージョンが食い違っていたりして悩んだこともありました。
最終的に、現時点での最新版である5.4.1を採用することで解決しました。本プロジェクト作成に費やした時間の8割くらいはここで適切なライブラリを選定することで消費されています。まだexperimentalなのでこのあたりはどうしてもしょうがないですね。

suspendなmethodをSwiftから呼び出せない

現状Kotlin/Nativeはsuspendなmethodをswiftから呼び出せるインターフェースを作ってくれないようです。suspendをやめ、Deferredインスタンスを返すようにすることでSwiftからもきちんと扱えるようになりました。こちらであればSwift側からも扱えるインターフェースが提供されており、問題なく呼び出すことが出来ます。

Kotlin/Nativeでビルドした際にランタイムでIncorrectDereferenceExceptionが発生する

 本プロジェクトでは非プリミティブ型のトップレベル変数をメインスレッド外から触ろうとしたために発生しました。具体的にどこで発生したかというと、ここです。対処法としては@ThreadLocalを付与して、スレッドローカルとして扱うことで対応を行いました。

終わりに

 これでKotlinでコードを一つ書けばAndroid/iOS両方で動くライブラリを作ることが出来ました。これでより業務を加速し、同じことを二回も書くようなつまらない作業は減らして、もっと本質の部分に時間を使うことが出来るようになるといいなぁと思います。

参考

JetBrains/kotlin-mpp-example
Multiplatform Project: iOS and Android
kotlin-native /IMMUTABILITY.md
DroidKaigi/conference-app-2019
SimonSchubert/Newsout

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

Android の DatePickerDialog で日時範囲(開始日時~終了日時)の選択

目的

Android の DatePickerDialog を使って、日付範囲(開始日~終了日)を選択させる。

  • DatePickerDialogで日付を選んで、Buttonのtextに表示させる。
    • Buttonじゃなくて他のViewに表示させてもOK。TextViewとか。
  • 選択できる日付を制限する。
    • 開始日に終了日より後の日付を選択させない。
    • 終了日に開始日より前の日付を選択させない。

その他の仕様(詳しい説明はこちらの記事に書いてます)

すでに日付が選択されており、DatePickerDialogで日付を選ばなかったとき、選択された日付を削除する。

  • キャンセルを押したときにButton.textにセットされていた値を変更するということ。
  • ここでは日付が未入力の場合、"指定なし"というテキストをセットすることにします。

動作環境

  • Kotlin 1.3.21
  • Android 8.0 (Oreo)
  • AndroidStudio 3.3.2

動作イメージ

dataRangeTest.gif

実装

layoutファイル

開始日付と終了日付を選択・表示するButtonとTextViewの構成とします。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".MainActivity">
    <TextView
            android:text="日付範囲"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/date_range_text"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
    <Button
            android:text="指定なし"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/begin_date_button"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/date_range_text" android:layout_marginLeft="64dp"
            android:layout_marginStart="64dp" android:layout_marginTop="16dp"/>
    <TextView
            android:text="~"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/range"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/begin_date_button" android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
    <Button
            android:text="指定なし"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/end_date_button"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/range" android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
</android.support.constraint.ConstraintLayout>

activityファイル

以下は、MainActivity.onCreateメソッド内のコードですが、分かりやすくするために2つに分けます。

  • dateSetOnClickListener.kt : ButtonのViewを取得とButton.setOnClickListenerの実装を行う。
  • DateDialogFragment.kt : DateDialogFragmentを表示させるclass。
dateSetOnClickListener.kt
val mBeginDate = findViewById<Button>(R.id.begin_date_button)
val mEndDate = findViewById<Button>(R.id.end_date_button)

/* 開始日付を指定するボタンを押したとき */
mBeginDate.setOnClickListener {
//    DialogFragmentを生成し、表示
    DateDialogFragment(mBeginDate).show(supportFragmentManager, mBeginDate::class.java.simpleName)
}
/* 終了日付を指定するボタンを押したとき */
mEndDate.setOnClickListener {
    DateDialogFragment(mEndDate).show(supportFragmentManager, mEndDate::class.java.simpleName)
}
DateDialogFragment.kt
/* 日付を入力する時に使用するinnerクラス
   DialogFragmentで日付を入力できるカレンダーを表示する
   DialogFragmentを呼び出したボタンを取得したいので、コンストラクタの引数にButtonを定義する */
class DateDialogFragment(val button: Button) : DialogFragment() {
    /* DatePickerDialogを返却するメソッド
       このメソッドで、日付を選択した後の処理や、日付範囲、Dialogのタイトルを設定する */
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
//        現在日付を取得
        val calendar = Calendar.getInstance()
        return DatePickerDialog(
            context,
            theme,
//            DialogFragmentで日付を選択し、OKを押したときの処理
            DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
                val inputDate = Calendar.getInstance()
//                選択された日付を取得
                inputDate.set(year, month, dayOfMonth)
                val dfInputeDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
//                CalendarからStringへ変換
                val strInputDate = dfInputeDate.format(inputDate.time)
//                DialogFragmentを呼び出したボタンのテキストに日付をセット
                button.text = strInputDate
            },
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH),
            calendar.get(Calendar.DAY_OF_MONTH))
            .also {
                /* 選択可能な日付の上限を設定
                   終了日付が指定されている場合、開始日付は終了日付より後を選べないようにする */
                if (button === mBeginDate && mEndDate.text != "指定なし"){
                    val maxDate = Calendar.getInstance()
                    val dfMaxDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN)
//                    指定されている終了日付を取得
                    val endInputDate = dfMaxDate.parse(mEndDate.text.toString())
                    maxDate.time = endInputDate
//                    選択可能な開始日付の上限に指定されている終了日付を設定
                    it.datePicker.maxDate = maxDate.timeInMillis
                } else {
//                    選択可能な開始日付の上限に現在日付を設定
                    it.datePicker.maxDate = calendar.timeInMillis
                }
                /*  選択可能な日付の下限を指定 */
                val minDate = Calendar.getInstance()
                /* 開始日付が指定されている場合、終了日付は開始日付より前を選べないようにする */
                if (button === mEndDate && mBeginDate.text != "指定なし"){
                    val dfMinDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN)
                    val beginInputDate = dfMinDate.parse(mBeginDate.text.toString())
                    minDate.time = beginInputDate
//                    選択可能な終了日付の下限に指定されている開始日付を設定
                    it.datePicker.minDate = minDate.timeInMillis
                } else {
//                    選択可能な終了日付の下限を設定、ここでは2018/2/1とします
                    minDate.set(2018, 2, 1)
                    it.datePicker.minDate = minDate.timeInMillis
                }
//                タイトルが勝手に表示されるのを防ぐために空文字をセット
                it.setTitle("")
            }
    }

    /* DialogFragmentでcancelを押した際の処理 */
    override fun onCancel(dialog: DialogInterface?) {
        super.onCancel(dialog)
//        cancelを押した際はセットされていた日付を削除し、"指定なし"をセット
        button.text = "指定なし"
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【kotlin】okhttp3でログを出力する

API通信まわりの実装をする際は、ログを見れたほうが作業が捗りますよね。
今回はokhttp3を使ってログを出力する方法をご紹介します。

  • macOS: 10.14.1
  • Android Studio: 3.4
  • kotlin: 1.3.21

手順

build.gradleへの追加

logging-interceptorを追加します。

app/build.gradle
def okhttp_version = '3.11.0'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" // ログ出力に必要

OkHttpClientの設定

ランタイムなどの設定と一緒に、.addInterceptor...の箇所を追加します。

init {
    val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        // ログを出力させる設定
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .build()
}

手順は以上です。

出力してみる

スクリーンショット 2019-05-06 15.42.44.png
こんな感じで、LogcatにD/OkHttp:のメッセージが吐き出されるようになれば成功です。
通信したURLや、GET/POST、ステータスコードや受け取ったレスポンスなど、色々表示してくれて便利です。

※もし手順通りに設定したにもかかわらずログが出力されない場合は、通信以前のところでつまずいている可能性があるので(例えばインターネット通信の許可をしていないとか)確認してみてください。

参考

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