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

Spring×Kotlin×MySQL×Mybatis×ThymeleafでサンプルWebアプリ作ってみた

実務では基本的にJavaでWebアプリを作ることが多いのですが
Kotlin使ったらどうなのかと思いちょっと勉強してみました。

DBはMySQL, ビルドにGradleを使用し、
IDEはIntelliJ Community Editionを使用します。
(ちなみにEclipse or STSにKotlinプラグインを入れて動かすことも可能ですが、IntelliJの方が便利な面が多そうなのであまりおすすめできないです。IntelliJはCommunity Editionであれば無料で使えます)

出来上がったものがこちら

プロジェクトを作成するまでの準備

Spring Initializrで雛形作成

以下のサイトにアクセス
https://start.spring.io/

今回は以下の設定でプロジェクトを作成しました。

項目 設定値
Project Gradle
Language Kotlin
Spring Boot 2.1.4
Packaging Jar ※組み込みTomcatを使用する場合はJarで良いみたいです
Java Version 8
Dependencies DevTools, Web, Thymeleaf, MySQL, JDBC, MyBatis

その他の設定はお好みで。
ダウンロードが終わったら展開し、IntelliJにインポートしましょう。

DB

以下のDBを作成します。

-- DB作成
CREATE DATABASE sample; 
USE sample;
-- テーブル作成
CREATE TABLE item(
    id INTEGER PRIMARY KEY AUTO_INCREMENT, 
    name VARCHAR(10), 
    price INTEGER, 
    type INTEGER
);
-- データ挿入
INSERT INTO 
    item(name, price, type) 
VALUES
    ('Apple', 150, 1), 
    ('Onion', 60, 2), 
    ('Cherry', 300, 1), 
    ('Carrot', 70, 2), 
    ('Banana', 80, 1)
;

実装

ディレクトリ構成は以下のようになります。

ディレクトリ構成
src
├── main
│   ├── kotlin
│   │   └── com
│   │       └── example
│   │           └── sample
│   │               ├── SampleApplication.kt // デフォルトのまま
│   │               ├── ServletInitializer.kt // デフォルトのまま
│   │               ├── controller
│   │               │   └── SampleController.kt
│   │               ├── mapper
│   │               │   └── SampleMapper.kt
│   │               ├── model
│   │               │   └── SampleModel.kt
│   │               └── service
│   │                   └── SampleService.kt
│   └── resources
│       ├── application.yml
│       ├── messages.properties
│       ├── mybatis
│       │   └── mapper
│       │       └── SampleMapper.xml
│       ├── static
│       │   ├── css
│       │   │   └── style.css
│       │   └── js
│       └── templates
│           └── index.html
└── test // デフォルトのまま(今回触らないので省略)

ControllerからServiceを呼んで
その中でMapperを使ってDBにアクセスし、Modelを返し、Viewに表示するという
よくあるパターンで作っていきます。

設定ファイル

とりあえずapplication.propertiesをymlにして以下のように設定する。
(ymlにしたのは私の好みです)

application.yml
spring:
  datasource:
    url: # TODO Set connection string  ex. jdbc:mysql://localhost:3306/sample
    username: # TODO Set username
    password: # TODO  Set password
    driverClassName: com.mysql.jdbc.Driver

mybatis:
  mapper-locations: classpath*:/mybatis/mapper/*.xml
  type-aliases-package: com.example.sample.model
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 30
    cache-enabled: false

ポイントとしては

  • datasourceを設定する。
  • 「mapper-locations」にmybatisのMapper(.xml)の場所を設定する。
  • 「type-aliases-package」にMapperで扱うModelクラスを配置する場所を設定する。

くらいかと思います。その他の設定はよく使いそうなものをとりあえず書いてます。

Model

テーブルに対応したModelを作成します。

SampleModel.kt
package com.example.sample.model

data class SampleModel(var id: Int, var name: String, var price: Int, var type: Int)

JavaだとGetter、SetterやらConstructorなどいっぱい書かないといけないのが
Kotlinだとなんと一行で書けちゃいます。

上記の場合だと、
id(Int型), name(String型), price(Int型), type(Int型)というフィールドを持った
SampleModelというクラスができます。もちろんGetter, Setter, Constructorも込みです。

また、Modelのようにデータを格納するためだけのクラスには
data修飾子を付けておくのがKotlinでは慣習のようです。
これを付けておくとtoStringやcopyなどの関数を自動的に実装してくれるみたいです。便利ですね。

Mapper

次にMapperです。
Mapperアノテーションを付与したMapperインターフェースを作成します。

SampleMapper.kt
package com.example.sample.mapper

import org.apache.ibatis.annotations.Mapper
import com.example.sample.model.SampleModel

@Mapper
interface SampleMapper {
    // 抽象メソッドで定義する
    fun find(): List<SampleModel>

}

Kotlinではfunキーワードでメソッド、関数を定義できます。
ここではSampleModelのリストを返すfind抽象メソッドを定義しています。
Mybatisを使うとこれらの抽象メソッドが自動で実装されます。
次にこのMapperインターフェースに対応したSQLが書かれたxmlファイルを作成します。

SampleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sample.mapper.SampleMapper">

    <select id="find" resultType="SampleModel">
        SELECT
            id,
            name,
            price,
            type
        FROM
            item
    </select>

</mapper>

namespaceに対象のMapperインターフェースのパス、
idにメソッド名、resultTypeに戻り値の型を設定します。
今回はapplication.ymlにtype-aliases-packageを設定しているので
resultTypeは完全修飾クラス名ではなくクラス名だけで動くようになっています。

Service

次にServiceです。

SampleService.kt
package com.example.sample.service

import com.example.sample.mapper.SampleMapper
import com.example.sample.model.SampleModel
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class SampleService(private val mapper: SampleMapper) {

    fun find() : List<SampleModel> = mapper.find()

}

constructorキーワードの前に@Autowiredを付与することで
SampleMapperをインジェクトしています。
上記のようにコンストラクタに値を渡すだけでインジェクトできるみたいです。
こちらもJavaより簡単に書けて楽そうですね。

Controller

次にControllerです。

SampleController.kt
package com.example.sample.controller

import com.example.sample.service.SampleService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping

@Controller
class SampleController {

    @Autowired
    lateinit var service: SampleService

    @GetMapping("/")
    fun index(model: Model): String  {
        val modelList = service.find()
        model.addAttribute("modelList", modelList)
        return "index"
    }
}

こちらもSampleServiceをインジェクトしています。
Serviceでは省略しましたが、本来KotlinではAutowiredするときにlateinitキーワードをつける必要があります。
ですが、もっと簡単に以下のように書くこともできます。

@Controller
class SampleController(private val service: SampleService){

    @GetMapping("/")
    fun index(model: Model): String  {
        // 以下3行とも同じように動く
        // model.addAttribute("modelList", service.find())
        // model.set("modelList", service.find()) 
        model["modelList"] = service.find()
        return "index"
    }
}

View

Controllerで設定した値を表示します。

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="../static/css/style.css" th:href="@{/css/style.css}">
    <title>Kotlin Spring Sample</title>
</head>
<body>

<h1>Model List</h1>

<table>
    <tr>
        <th th:text="#{title.id}">No</th>
        <th th:text="#{title.name}">Name</th>
        <th th:text="#{title.price}">Price</th>
        <th th:text="#{title.type}">Type</th>
        <th th:text="#{title.note}">Note</th>
    </tr>
    <th:block th:each="model : ${modelList}">
        <tr>
            <td th:text="${model.id}"></td>
            <td th:text="${model.name}"></td>
            <td th:text="${model.price}"></td>
            <td th:text="${model.type}"></td>
            <!--/* model.toString()が実行される */-->
            <td th:text="${model}"></td>
        </tr>
    </th:block>
</table>

</body>
</html>

実行結果

スクリーンショット 2019-05-04 16.14.04.png

ハマったところ

STSで頑張ろうとしてハマる

私はIntelliJをそこまで使い込んだことがなかったので、最初STSを使おうとしていました。
STSにKotlin Pluginを入れ、Spring Initializrで作ったプロジェクトを入れて動かそうとすると
…動かない。
GithubやQiitaで上がっているサンプルコード等をImportしてもそのままでは動かず…。
どうやらKotlinでは継承元やオーバーライド元のクラスや関数にopenという修飾子をつけるようで
参考にしていたコードには軒並みopen修飾子が付いておらずエラーになっていました。
IntelliJを使うと自動的にopen修飾子をつけるプラグインが動くようです。(STSでもできるのかもしれないですが、設定めんどくs...)
他にも色々イマイチなところが多かったので、最初から大人しくIntelliJ使っとけばよかった…。

IntelliJでGradleが動かない…

これは最後までよくわからなかったのですが
Spring initializrで作成したプロジェクトをインポートしても
「No such property: GradleVersion for class: JetGradlePlugin」
みたいなエラーが出て動かせなかったです。
スクリーンショット 2019-05-02 11.12.54.png

色々調べてみたら、gradle/wrapper/gradle-wrapper.propertiesで設定している
gradleのバージョンを下げると動く、みたいな記事を見つけたので
その通りにしたらとりあえず動きました。

gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# 変更前
# distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
# 変更後
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Mybatisの設定周りで混乱する

これはKotlinに移行したからとか関係なく完全に自分の知識不足なのですが、
Mybatisの設定を調査するのにとても時間がかかりました。
application.ymlにはSpringの設定しか書けないという先入観があり、
Configurationアノテーションを使おうとしたり、SqlSessionTemplateクラスを使おうとしたり
色んな手法で迷いまくったあげく、最終的にはapplication.ymlに全て記載するのに落ち着きました。

mybatis-spring-boot-starterを使うと以下のようにapplication.ymlに記載するだけで動くみたいです。

application.yml
spring:
  datasource:
    url: # TODO Set connection string  ex. jdbc:mysql://localhost:3306/sample
    username: # TODO Set username
    password: # TODO  Set password
    driverClassName: com.mysql.jdbc.Driver

# 以下Mybatisの設定
mybatis:
  mapper-locations: classpath*:/mybatis/mapper/*.xml
  type-aliases-package: com.example.sample.model
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 30
    cacheEnabled: false
build.gradle(抜粋)
dependencies {
    // (中略) こんな感じに設定しておく(Spring InitializrでMybatisを入れると追加されているはず)
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.0.1'

}

参考文献等

https://qiita.com/kazuki43zoo/items/ea79e206d7c2e990e478
https://www.shookuro.com/entry/2017/11/23/203318
https://qiita.com/k5n/items/18adb5c3503a54e96c22
https://stackoverflow.com/questions/53600179/errorno-such-property-gradleversion-for-class-jetgradleplugin

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

MotionLayoutでMaterialDesignのTextFieldもどきを実装する

はじめに

昨年のGoogleI/O 2018でConstraintLayout2.0が紹介されましたが、その中でMotionLayoutという新たなAndroidのアニメーションの仕組みが公開されました。
今回は、そのMotionLayoutを使用して、MaterialDesignのTextFieldっぽいものを実装してみたので、それについて実装方法などを書き連ねたいと思います。
※またMotionLayoutはまだalpha版なので、今回は、constraint-layout:2.0.0-alpha4での実装を紹介しますが、新たなバージョンがリリースされた場合、実装が変わる可能性もあるので、ご了承ください。

作ったもの

こちらのような項目がリストのように表示されていて、画面遷移させることなく閲覧モードから入力モードにすることができます。それぞれのモード間の遷移時のアニメーションでMotionLayoutを使って実装しています。

挙動としては、
1. 右のテキスト部分をクリックした場合、入力モードになる
2. フォーカスが外れたら、閲覧モードに戻る
(フォーカスは別のテキストFieldをクリックするか、下のボタンを押下すると外れる)

ソースコードはこちらに上げています。参考にされたい方はこちらをご覧ください。
https://github.com/youmitsu/MotionLayoutMaterialTextField

MotionLayoutの概要

MotionLayoutについては、多くの方が仕組みについての記事を公開しているので、ここでは省略します。

基本的なステップとしては、以下となります。

  1. アニメーションさせる前のレイアウトをMotionLayoutを親として実装
  2. Sceneのxmlを作成
  3. 1のattributeに2のSceneファイルを指定

実装方法

1. ConstraintLayout2.0のインストール

まずは、ConstraintLayout2.0をインストールします。
AndroidXと、SupportLibraryの2つのArtifactsがあるので、自身のプロジェクトに応じて、使い分けましょう。

リリースノートはこちら
https://androidstudio.googleblog.com/2019/04/constraintlayout-200-alpha-4.html

AndroidXの場合

dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}

SupportLibraryの場合

dependencies {
    ...
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha4'
}

2. 初期状態のviewを作成

まずは、こちらのクリックされる前のレイアウトを作っていきます。

xmlは以下になります。MotionLayoutはConstraintLayoutの子クラスなので、基本的にはConstraintLayoutでレイアウトを作るのと同じ要領で実装します。

※サンプルリポジトリでは、Databindingを使って入力データをバインディングしているので、<layout><data>タグが入っていますが、無くても動きます。

layout_custom_motion_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
    >

        <TextView
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:gravity="start|center_vertical"
                android:layout_marginStart="10dp"
                android:textColor="#808080"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toStartOf="@id/nickname_value"
                android:text="@{data.title}"
        />

        <TextView
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:gravity="end|center_vertical"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                android:ellipsize="end"
                android:singleLine="true"
                android:maxEms="15"
                android:text="@{data.value}"
                android:clickable="true"
                android:focusable="true"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                android:inputType="text"
                android:text="@{data.value}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

    </androidx.constraintlayout.motion.widget.MotionLayout>
</layout>

3. アニメーション前後のConstraintの状態とTransitionを表すMotionSceneファイルを作成

次に、タップしてアニメーションし始める時と終わった時のレイアウトのConstraintSetとTransitionをMotionSceneという形で定義します。
MotionSceneファイルはxmlで記述するので、res/xml配下に新たなファイルを作成します。

xmlファイルは以下です。
@id/startがアニメーションする前のConstraintSet。@id/endがアニメーションした後のConstraintSetを表しています。
今回の場合、start時のConstraintと上記2で定義したMotionLayout内の要素のConstraintは一致することになります。

@id/startのConstraintSetでのレイアウト:

@id/endのConstraintSetでのレイアウト:

layout_custom_motion_edittext_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
        xmlns:motion="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
            motion:constraintSetStart="@id/start"
            motion:constraintSetEnd="@id/end"
            motion:duration="200">

        <OnClick motion:targetId="@id/nickname_value"
                 motion:clickAction="transitionToStart|transitionToEnd"/>

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textSize="@dimen/text_size_normal"
                android:layout_marginStart="5dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toStartOf="@id/nickname_value">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="15"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="#808080"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <Constraint
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

        <Constraint
                android:id="@id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                motion:layout_constraintBottom_toTopOf="@id/edit_text"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="12"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="@color/colorAccent"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:textSize="15sp"
                android:alpha="0"
                android:visibility="invisible"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"/>

        <Constraint
                android:id="@id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="1"
                android:visibility="visible"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

</MotionScene>

4. MotionLayoutのattributeにlayoutDescriptionとして3のSceneファイルを設定

アニメーションする対象のview(MotionLayout)とMotionSceneファイルを作成できたら、それぞれを結びつけるために、MotionLayoutのlayoutDescription属性を設定します。こうすることで、画像のようなアニメーションが実現できます。

layout_custom_motion_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
            app:layoutDescription="@xml/layout_custom_motion_edittext_scene"  // これを追加
    >
    ...
    </androidx.constraintlayout.motion.widget.MotionLayout>
</layout>

まとめ

今回は、MotionLayoutを使って、MaterialDesignのTextFieldの拡張っぽいものを実装してみました。アニメーションに関するコードをほとんどxmlだけで完結させることができるので、とても使いやすそうだなという印象です。
これからも色々機能追加があるみたいなので、楽しみです。
最後まで読んでいただきありがとうございました。

参考にさせていただいたもの

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

【Android】MotionLayoutでMaterialDesignのTextFieldもどきを実装する

はじめに

昨年のGoogleI/O 2018でConstraintLayout2.0が紹介されましたが、その中でMotionLayoutという新たなAndroidのアニメーションの仕組みが公開されました。
今回は、そのMotionLayoutを使用して、MaterialDesignのTextFieldっぽいものを実装してみたので、それについて実装方法などを書き連ねたいと思います。
※またMotionLayoutはまだalpha版なので、今回は、constraint-layout:2.0.0-alpha4での実装を紹介しますが、新たなバージョンがリリースされた場合、実装が変わる可能性もあるので、ご了承ください。

作ったもの

こちらのような項目がリストのように表示されていて、画面遷移させることなく閲覧モードから入力モードにすることができます。それぞれのモード間の遷移時のアニメーションでMotionLayoutを使って実装しています。

挙動としては、
1. 右のテキスト部分をクリックした場合、入力モードになる
2. フォーカスが外れたら、閲覧モードに戻る
(フォーカスは別のテキストFieldをクリックするか、下のボタンを押下すると外れる)

ソースコードはこちらに上げています。参考にされたい方はこちらをご覧ください。
https://github.com/youmitsu/MotionLayoutMaterialTextField

MotionLayoutの概要

MotionLayoutについては、多くの方が仕組みについての記事を公開しているので、ここでは省略します。

基本的なステップとしては、以下となります。

  1. アニメーションさせる前のレイアウトをMotionLayoutを親として実装
  2. Sceneのxmlを作成
  3. 1のattributeに2のSceneファイルを指定

実装方法

1. ConstraintLayout2.0のインストール

まずは、ConstraintLayout2.0をインストールします。
AndroidXと、SupportLibraryの2つのArtifactsがあるので、自身のプロジェクトに応じて、使い分けましょう。

リリースノートはこちら
https://androidstudio.googleblog.com/2019/04/constraintlayout-200-alpha-4.html

AndroidXの場合

dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}

SupportLibraryの場合

dependencies {
    ...
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha4'
}

2. 初期状態のviewを作成

まずは、こちらのクリックされる前のレイアウトを作っていきます。

xmlは以下になります。MotionLayoutはConstraintLayoutの子クラスなので、基本的にはConstraintLayoutでレイアウトを作るのと同じ要領で実装します。

※サンプルリポジトリでは、Databindingを使って入力データをバインディングしているので、<layout><data>タグが入っていますが、無くても動きます。

layout_custom_motion_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
    >

        <TextView
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:gravity="start|center_vertical"
                android:layout_marginStart="10dp"
                android:textColor="#808080"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toStartOf="@id/nickname_value"
                android:text="@{data.title}"
        />

        <TextView
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:gravity="end|center_vertical"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                android:ellipsize="end"
                android:singleLine="true"
                android:maxEms="15"
                android:text="@{data.value}"
                android:clickable="true"
                android:focusable="true"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                android:inputType="text"
                android:text="@{data.value}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

    </androidx.constraintlayout.motion.widget.MotionLayout>
</layout>

3. アニメーション前後のConstraintの状態とTransitionを表すMotionSceneファイルを作成

次に、タップしてアニメーションし始める時と終わった時のレイアウトのConstraintSetとTransitionをMotionSceneという形で定義します。
MotionSceneファイルはxmlで記述するので、res/xml配下に新たなファイルを作成します。

xmlファイルは以下です。
@id/startがアニメーションする前のConstraintSet。@id/endがアニメーションした後のConstraintSetを表しています。
今回の場合、start時のConstraintと上記2で定義したMotionLayout内の要素のConstraintは一致することになります。

@id/startのConstraintSetでのレイアウト:

@id/endのConstraintSetでのレイアウト:

layout_custom_motion_edittext_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
        xmlns:motion="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
            motion:constraintSetStart="@id/start"
            motion:constraintSetEnd="@id/end"
            motion:duration="200">

        <OnClick motion:targetId="@id/nickname_value"
                 motion:clickAction="transitionToStart|transitionToEnd"/>

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
                android:id="@+id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textSize="@dimen/text_size_normal"
                android:layout_marginStart="5dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toStartOf="@id/nickname_value">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="15"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="#808080"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:textSize="15sp"
                android:layout_marginEnd="10dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toEndOf="@id/nickname_title"/>

        <Constraint
                android:id="@+id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="0"
                android:visibility="invisible"
                android:textSize="15sp"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

        <Constraint
                android:id="@id/nickname_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                motion:layout_constraintBottom_toTopOf="@id/edit_text"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent">
            <CustomAttribute
                    motion:attributeName="textSize"
                    motion:customFloatValue="12"
            />
            <CustomAttribute
                    motion:attributeName="textColor"
                    motion:customColorValue="@color/colorAccent"
            />
        </Constraint>

        <Constraint
                android:id="@+id/nickname_value"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:textSize="15sp"
                android:alpha="0"
                android:visibility="invisible"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"/>

        <Constraint
                android:id="@id/edit_text"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:alpha="1"
                android:visibility="visible"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

</MotionScene>

4. MotionLayoutのattributeにlayoutDescriptionとして3のSceneファイルを設定

アニメーションする対象のview(MotionLayout)とMotionSceneファイルを作成できたら、それぞれを結びつけるために、MotionLayoutのlayoutDescription属性を設定します。こうすることで、画像のようなアニメーションが実現できます。

layout_custom_motion_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="data" type="jp.co.youmitsu.myapplication.CustomMotionEditTextLayout"/>
    </data>

    <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/motion_layout"
            app:layoutDescription="@xml/layout_custom_motion_edittext_scene"  // これを追加
    >
    ...
    </androidx.constraintlayout.motion.widget.MotionLayout>
</layout>

まとめ

今回は、MotionLayoutを使って、MaterialDesignのTextFieldの拡張っぽいものを実装してみました。アニメーションに関するコードをほとんどxmlだけで完結させることができるので、とても使いやすそうだなという印象です。
これからも色々機能追加があるみたいなので、楽しみです。
最後まで読んでいただきありがとうございました。

参考にさせていただいたもの

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

Forge式BlockStateを利用してItemのJsonModelを削減する方法

概要

例えばmetadataをもつItemを追加するにはそのmetadataの個数(今回は4つ)だけのjsonファイルが必要だったが、それを2個で済ませる(同じModelを使いまわす)方法

環境

Minecraft-1.10.2
Minecraft-1.11.2
Minecraft-1.12.2

(*試していないだけでおそらく他バージョンでも可)

追加するJSONファイル

(以下、modidはそのItemを追加するmodのID)

まずassets/modid/models/block/に以下のJSONファイルを作る
このときitemではなくblockのModelの位置に追加するのを間違えないように

item_generated.json
{
  "parent": "item/generated"
}

これは普通の板状に表示されるItemの場合で、ツール等ちゃんと持たせたい場合はgeneratedhandheldに。

その他のmodel(自作も含む)の場合も同じようにassets/modid/models/block以下に置いてModelを指定すれば使える。

次にassets/modid/blockstates/に以下のJSONファイルを作る
(variantsには追加するmeta数だけ書き加える)

sample_item.json
{
  "forge_marker": 1,
  "defaults": {
    "model": "modid:item_generated",
    "transform": "forge:default-item",
    "uvlock": true
  },
  "variants": {
    "meta0": [{
      "textures": {
        "layer0": "modid:items/sample_item_0"
      }
    }],
    "meta1": [{
      "textures": {
        "layer0": "modid:items/sample_item_1"
      }
    }],
    "meta2": [{
      "textures": {
        "layer0": "modid:items/sample_item_2"
      }
    }],
    "meta3": [{
      "textures": {
        "layer0": "modid:items/sample_item_3"
      }
    }]
  }
}

テクスチャ指定はlayer0, layer1と数字が上がるごとに上に重ねて表示される(何枚いけるのかは不明)。

Modelの登録(kotlin)

次は先ほど追加したModelの登録。

ClientProxy.kt
for (i in 0 until 4) {
    ModelLoader.setCustomModelResourceLocation(sampleItem, i,
            ModelResourceLocation(ResourceLocation(modid, "sample_item"), "meta$i"))
}

javaの場合も同じように

ClientProxy.java
for (int i = 0; i < 4; i++) {
    ModelLoader.setCustomModelResourceLocation(sampleItem, i,
            new ModelResourceLocation(new ResourceLocation(modid, "sample_item"), "meta" + i));
}

最後に

おそらくModelResourceLocationの第2引数を弄ればBlockと同じように、

sample_item.json
{
  "forge_marker": 1,
  "defaults": {
    "model": "modid:item_generated",
    "transform": "forge:default-item",
    "uvlock": true
  },
  "variants": {
    "meta": {
      "0":{   },
      "1":{   },
      "2":{   },
      "3":{   }
  }
}

のような形にもできると思うけどめんどくさいからやめた

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

UnityのスマホネイティブプラグインをKotlin/Nativeで共通化する

Unityのスマホネイティブプラグインを言語統一できないか?

昨今のスマホネイティブプラグインの言語の選択肢としてはJava, Kotlin, Objective-c, Swiftがあげられるかと思います。
たまに自分も趣味などでプラグインを使うことがあるのですが同じような処理を別で書かないといけないのが手間だと思っていました。
そこで昨年くらいからスマホのネイティブ界隈で話題になっていたKotlin/Nativeに目をつけてUnityで実行させてみたという記事です。

そもそもKotlin/Nativeとは…?
まとめている記事もありましたので参照させて頂きます。

基本的にはスマホネイティブの共通化できるロジックをKotlinで書いて共通化させようぜ!ってことなのですがAndroidでは.jarとしても吐き出せますし、iOSは.frameworkとして吐き出せるのでネイティブにとって扱いやすいものになっています。

さて、今回検証に使用したリポジトリです。動かして見たい人は是非ご活用ください。

実行した結果

Android iOS
Screenshot_20190502-040341.jpg Simulator Screen Shot - iPhone Xʀ - 2019-05-02 at 10.31.23.png

文字列をOS毎に変えるという処理ですが呼び出すメソッドは1つにしてあります。

準備

Kotlin/Native自体の作成方法は既にわかりやすい記事がありますのでそちらを参照させていただきます。
Kotlin/Nativeチュートリアル Android, iOS編
自分はここを参考にさせて頂きました。そのため今回上記の記事をベースに進めます。

Androidのネイティブが自分はよくわからなかったので、最初は基本的にコピペで作って必要な箇所を変えていきました。

今回、上記記事の「Common moduleの解説」の章まで出来たら一旦は大丈夫です。
上記の記事ほぼそのままですがソースコードを載せておきます。

共通

common.kt
package com.sample.mizotake.kotlinnativeforunity

expect fun platformName(): String

public class common {
    public fun createApplicationScreenMessage(): String {
        return "Call Kotlin Native on ${platformName()}"
    }
}

Android

actual.kt
package com.sample.mizotake.kotlinnativeforunity

actual fun platformName(): String {
    return "Android"
}

iOS

actual.kt
package com.sample.mizotake.kotlinnativeforunity

import platform.UIKit.UIDevice

actual fun platformName(): String {
    return UIDevice.currentDevice.systemName() +
            " " +
            UIDevice.currentDevice.systemVersion
}

共通処理にUnityで呼び出すクラスとメソッドを定義します。OS毎に変える処理はexpect actual処理でinterfaceのように切り出して呼べるようです。

Unityへの導入

Android

自分もよく把握できていませんがGradle Syncをするとbuildというディレクトリができて
スクリーンショット 2019-05-02 11.06.40.png

プロジェクト名 + android.jarができていました。
もし出来ていない場合は
スクリーンショット 2019-05-02 11.04.44.png
右端にGradleというタブがあるのでそこからbuildの項目を見るとbuildの詳細一覧があるのでandroidJarをダブルクリックすれば走り出してjarができるかと思います。

吐き出されたjarをUnityのPlugin/Androidに放り入れるだけです。
これでUnityへの導入は完了です。

iOS

こちらは先ほどのKotlin/Nativeチュートリアル Android, iOS編の「iOSアプリ」の章にあるbuild.gradleの追記だけ行いましょう。

/common/build.gradle
...

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'

    inputs.property "mode", mode
    dependsOn kotlin.targets.iOS.compilations.main.linkTaskName("FRAMEWORK", mode)

    from { kotlin.targets.iOS.compilations.main.getBinary("FRAMEWORK", mode).parentFile }
    into frameworkDir

    doLast {
        new File(frameworkDir, 'gradlew').with {
            text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
            setExecutable(true)
        }
    }
}

tasks.build.dependsOn packForXCode

ここの部分ですね。これを追記して./gradlew buildすることで
スクリーンショット 2019-05-02 11.17.06.png
main.frameworkができます。これをUnityのPlugin/iOSに放り込めばframeworkの導入は大丈夫ですが、iOSの場合もう一手間必要です。
externの実装がないとC#では呼び出せませんそのためPlugin/iOSフォルダに

common.mm
#import <main/main.h>

extern "C" {
    const char* createApplicationScreenMessage() {
        NSString *message = [[Maincommon alloc] init].createApplicationScreenMessage;
        return strdup([message UTF8String]);
    }
}

を追加しましょう。これを追加することで先ほど作ったKotlinで書いたコードのframeworkを参照できます。
ここでcommonというkotlinファイルを作ったがMaincommonって何だろう?ってなると思います。どうやらframeworkに吐き出す時に変換されているようです。
それを確認するにはAndroidStudioでframeworkのHeaderを見ると一番下の行に自分で実装した処理が追記されていると思います。
スクリーンショット 2019-05-02 11.22.16.png

これを参考にしてObjective-c++でインターフェースを定義する必要があります。
ちなみにSwiftだとMaincommonという変換名ではなくcommonで呼び出せそうですがSwiftを使うために手間をかけるよりObjective-c++を書いた方が早いと自分は思うのでこのまま進めます。

C#で呼び出す

事前準備は終わりました。
UnityではuGUIのTextにネイティブで呼び出した文字列を表示させます。

CallKotlinNative.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Runtime.InteropServices;

public class CallKotlinNative : MonoBehaviour
{

#if UNITY_IOS
    [DllImport("__Internal")]
    private static extern string createApplicationScreenMessage();
#endif

    private Text viewableText;

    void Start()
    {
        var pluginMessage = "";
#if UNITY_ANDROID
        using (var plugin = new AndroidJavaObject("com.sample.mizotake.kotlinnativeforunity.common"))
        {
            pluginMessage = plugin.Call<string>("createApplicationScreenMessage");
            Debug.Log(pluginMessage);
        }
#elif UNITY_IOS
        pluginMessage = createApplicationScreenMessage();
#endif
        viewableText = GetComponent<Text>();
        viewableText.text = pluginMessage;
    }
}

C#側は普通にネイティブプラグインを呼び出すだけですね。
これを実機ビルドまたはシミュレータービルドすることで動作の確認ができると思います。

終わりに

Kotlin/NativeのUnityProjectへの導入は手間が必要かと思っていましたが思った以上に簡単でした。ただ、iOSの導入のexternだけどうにかならなかいとAndroidProject内に.mm入れてみて.frameworkだけ吐き出して更新させるなどをしようと思いましたがうまく行きませんでした…Androidネイティブのディレクトリ構成やtaskのカスタムに詳しければどうにかできるのかな?と思っています。
Kotlin/Nativeを使えば基本的に言語はKotlinひとつに統一できますし、共通処理やOS依存処理も問題ないのではない気がしています。個人的にKotlinでUIKitなどもimportして使えることに驚きました。
何にせよ扱う言語は少ないに限ると思っていますのでKotlin/Nativeは良いものだと思います。ただ現在betaなので書き方や吐き出し方が変わる可能性は高いです。

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