Androidアプリ開発(Java, Kotlin両対応) これどうやって実装するんだっけ？ 〜Text編〜

この記事は「Androidアプリ開発　これどうやって実装するんだっけ？」の第二回です。

メインターゲットは私のような「そろそろググることを覚えた初中級者」を(勝手に)想定しています。  
JavaとKotlinの両方について解説しているので、「Javaでの実装はわかるけど、Kotlinだとどう書くの？」という人にも
お役に立つかと思います。

第一回は[Button編](https://qiita.com/daivr7774/items/3bed5793a124aa4a44aa)でした。

本企画のサンプルは下のリンクにあります。masterブランチがKotlinのソースコード、javaブランチがJavaのソースコードです。

https://github.com/Dai1678/SampleCodePrograms

# いろんなText、使ってみた

第一回でもうText使ってんじゃんというツッコミはなしで。
それくらい必須の機能ですね。

ただ単純にTextViewを置くのもつまらないので
サンプルとして、

- 文字を長押ししてコピーとかできるTextView
- ここだけ普通のEditText
- なんか補完機能で見つけた、TextClock
- みんな大好きMaterial Designが簡単に作れる、TexInputLayout/EditText

さらに、これらを使って簡単なログインフォームみたいな機能もつけました。

# 環境

Android Studio 3.0+
Android 8.0で動作確認

```build.gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.ddns.dai.samplecodeprograms"
        minSdkVersion 17
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.android.support:design:27.1.0'
    implementation 'com.android.support:customtabs:27.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
```

# 動作デモ

![TextSampleActivity.gif](https://qiita-image-store.s3.amazonaws.com/0/135833/371d403c-c788-254b-d66f-6ba62047fb8c.gif)

# XMLの解説

```activity_text_sample.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="10dp"
    tools:context=".TextSampleActivity">

    <TextView
        android:id="@+id/userName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="あなたのユーザー名は◯◯です\n長押しでコピーできます"
        android:textSize="20sp"
        android:textIsSelectable="true"/>

    <EditText
        android:id="@+id/editPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="パスワードを設定してください"
        android:textSize="20sp"
        android:maxLines="1"
        android:layout_marginTop="20dp" />

    <TextClock
        android:id="@+id/currentTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:hint="yyyy/MM/dd HH:mm:ss"
        android:textSize="20sp"
        android:timeZone="Asia/Tokyo"
        android:format12Hour="yyyy/MM/dd HH:mm:ss"
        android:format24Hour="yyyy/MM/dd HH:mm:ss"/>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/nameTextInputLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        app:counterEnabled="true"
        app:counterMaxLength="10"
        app:errorEnabled="true"
        app:hintAnimationEnabled="true"
        app:hintEnabled="true">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/userNameInput"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="ユーザー名"
            android:maxLines="1"
            android:textSize="20sp" />

    </android.support.design.widget.TextInputLayout>


    <android.support.design.widget.TextInputLayout
        android:id="@+id/passwordTextInputLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:counterEnabled="false"
        app:errorEnabled="false"
        app:hintAnimationEnabled="true"
        app:hintEnabled="true">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/userPassWordInput"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPassword"
            android:hint="パスワード"
            android:maxLines="1"
            android:textSize="20sp" />

    </android.support.design.widget.TextInputLayout>

    <Button
        android:id="@+id/loginButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="ログイン"/>

</LinearLayout>
```

`<TextView/>`で文字を表示できます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/135833/28ded038-88cf-eddb-53f1-5e6d442502e0.jpeg width=50%/>


- `android:textSize`の単位は`sp`が推奨されている。端末のフォントサイズ設定により伸縮される。
- `android:textIsSelectable`をtrueにすると表示されている文字をタップして範囲選択できる。
  
`<EditText/>`で文字の入力が行えます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/135833/b1f234e3-7d95-2126-1703-3ad6e63df801.jpeg width=50%/>

- `android:hint`で何も入力されていないときに、ユーザーに入力を促す文字列を設定できる。  
 (TextViewなどではAndroid StudioのPreview画面で確認する用に使っている)
- `android:maxLines`で入力中に改行しても、指定行数までしか入力スペースが拡大されない。  
 (文字が見えなくなるだけで実際は改行されている)


`<TextClock/>`は現在の日付や時刻を書式設定された文字列として表示できます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/135833/e0369a5d-c4b2-c087-dddf-65db5d16351d.jpeg width=50%/>

- `android:format12Hour`と`android:format24Hour`で、それぞれ12時間形式・24時間形式での書式設定が行えます。  
 サンプルでは、yyyy(西暦)/MM(月)/dd(日) HH(時):mm(分):ss(秒)です。  


`<TextInputLayout/>`と`<TextInputEditText/>`はセットで使います(入力欄を2つ用意するときはそれぞれ2つ用意する)。
これを利用すると、マテリアルデザインなアニメーションを持ったEditTextが実装できます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/135833/fe4e2c7d-6c58-0df6-81e6-8557b6176852.jpeg width=50%/>


使う際はbuild.gradleに`implementation 'com.android.support:design:(version)`をお忘れなく。
また、xmlでapp:~を使う際は、`xmlns:app="http://schemas.android.com/apk/res-auto"`をお忘れなく。

- `app:counterEnabled`をtrueにすると右下に入力した文字数が表示される
- `app:counterMaxLength`を指定すると`0/10`といった感じで文字数制限が可視化できる  
 (これだけだと文字数オーバーしても入力し続けられる)
- `app:errorEnabled`をtrueにすると
- `app:hintEnabled`をtrueにすると、入力中に左上にhint文字列が表示される
- `app:hintAnimationEnabled`をtrueにすると、hint文字列にアニメーションが適用される


# Activityの解説
## Java編

TextViewやEditTextで表示されている文字や入力した文字を扱うときによく使うのが、
getTextとsetTextです。

```example.java
 //getTextの返り値はTextView型なので、toStringなどでCast(型変換)して代入すると良い
String str = textView.getText().toString();

textView.setText("文字列");
```
TextInputEditTextも基本的に同じ使い方です。

サンプルでは、generateUserNameメソッドで10桁の乱数を生成してユーザー名を勝手に決め、
「パスワードを設定してください」のhintがあるEditTextで、任意の文字列でパスワードを設定。
ログインボタンが押されると、「ユーザー名」と「パスワード」のhintがあるTextInputEditTextに入力された文字列とそれぞれ文字列比較される処理になっています。

### TextInputLayoutとTextInputEditTextについて

マテリアルデザインなEditTextを使いたいと思ったので使ってみました。

基本的にはxmlに追加すれば勝手に動いてくれるのですが、
コード側ではxmlで指定した`app:counterMaxLength`の数字よりも多く入力された場合に
エラーメッセージを表示させました。

```example.java
userNameInput.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {

            }

            @Override
            public void afterTextChanged(Editable editable) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
                if (charSequence.length() > nameTextInputLayout.getCounterMaxLength()){
                    nameTextInputLayout.setError("ユーザー名は10文字以内で入力してください");
                }else{
                    nameTextInputLayout.setError(null);
                }
            }


        });
```

userNameInputは、ユーザー名を入力する場所のTextInputLayoutになります。
これにListenerを追加し、TextWatcherで入力されている文字を監視しています。

beforeTextChanged(CharSequence charSequence, int start, int count,int after)

- 文字列が修正される直前に呼び出されるメソッド
    - CharSequence charSequence  
      現在EditTextに入力されている文字列
    - int start  
      charSequenceの文字列で新たに追加される文字列のスタート位置
    - int count  
      charSequenceの文字列の中で変更された文字列の総数
    - int after  
      新規に追加された文字列の数

onTextChanged(CharSequence charSequence, int start, int before, int count)

- 文字１つを入力した時に呼び出される
    - CharSequence charSequence  
      現在EditTextに入力されている文字列
    - int start  
      charSequenceの文字列で新たに追加される文字列のスタート位置
    - int before  
      削除される既存文字列の数
    - int count  
      charSequenceの文字列の中で変更された文字列の総数

afterTextChanged(Editable editable)

- 最後にこのメソッドが呼び出される
    - Editable editable  
      最終的にできた修正可能な、変更された文字列

エラーメッセージの表示はsetErrorメソッドを使い、何も表示しない場合は引数にnullを指定すれば大丈夫です。

テキスト関係以外の説明は省略しましたが、以下がソースコードになります。

```TextSampleActivity.java
public class TextSampleActivity extends AppCompatActivity {

    private final int NoInput = -2;
    private final int LOGIN = 1;
    private final int ERROR = -1;

    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text_sample);

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        final String name = generateUserName();

        TextView userName = findViewById(R.id.userName);
        userName.setText("あなたのユーザー名は " + name + " です\n長押しでコピーできます");

        Button loginButton = findViewById(R.id.loginButton);
        final EditText editPassWord = findViewById(R.id.editPassword);
        final TextInputEditText userNameInput = findViewById(R.id.userNameInput);
        final TextInputEditText userPassWordInput = findViewById(R.id.userPassWordInput);

        final TextInputLayout nameTextInputLayout = findViewById(R.id.nameTextInputLayout);

        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String yourPassWord = editPassWord.getText().toString();
                String inputUserName = userNameInput.getText().toString();
                String inputUserPassWord = userPassWordInput.getText().toString();

                if (inputUserName.equals("")){
                    //yourPassWordの入力を促す
                    showResult(view, NoInput);
                }else{
                    boolean loginResult = name.equals(inputUserName) && yourPassWord.equals(inputUserPassWord);

                    if (loginResult){
                        //login
                        showResult(view, LOGIN);
                    }else{
                        //error
                        showResult(view, ERROR);
                    }
                }

            }
        });

        userNameInput.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {

            }

            @Override
            public void afterTextChanged(Editable editable) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
                if (charSequence.length() > nameTextInputLayout.getCounterMaxLength()){
                    nameTextInputLayout.setError("ユーザー名は10文字以内で入力してください");
                }else{
                    nameTextInputLayout.setError(null);
                }
            }


        });

    }

    private String generateUserName(){

        final String letters = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuilder stringBuilder = new StringBuilder();

        while (stringBuilder.length() < 10){
            int val = random.nextInt(letters.length());
            stringBuilder.append(letters.charAt(val));
        }

        return stringBuilder.toString();

    }

    @SuppressLint("SimpleDateFormat")
    private void showResult(View view, int result){

        String showText = "";
        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Date date = new Date(System.currentTimeMillis());

        switch (result){
            case NoInput:
                showText = "パスワードを設定してください";
                break;

            case LOGIN:
                showText = dateFormat.format(date) + "にログインしました!";
                break;

            case ERROR:
                showText = "入力されたユーザー名やパスワードが正しくありません";
        }

        Snackbar.make(view, showText, Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();

    }
}
```

editPassWord、userNameInput、userPassWordInputと名付けた変数になぜfinal修飾子を付けなければならなかったかというと、
匿名クラスを定義しているOnClickListenerなどの処理中に値が変更されるのを防ぐためですね。
もし処理中にカウントアップのような処理をしたい場合は、クラスのメンバ変数を使えばいいと思います。

## Kotlin編

KotlinでのgetText、setTextに当たる書き方はちょっとイケてます。

```example.kt
val str = textView.text.toString()  //setTextとして使用

textView.text = "文字列"  //getTextとして使用
```

なんと両方共、.textで済ませることができます。
特にgetTextとして使用するときは、javaのように引数にString型を渡すのではなく
代入という形で表現します。

余談ですが、Kotlinでは文字列中に変数を組み込みたいときに`$`を使うことで自然に表現できます。PHPとかと一緒ですね。

```example.kt
userName.text = "あなたのユーザー名は${name}です\n長押しでコピーできます"

//変数の前後にスペースを入れるときはこれで大丈夫
userName.text = "あなたのユーザー名は $name です\n長押しでコピーできます" 
```

以下、Kotlinでのサンプルアプリのソースコードになります。

```TextSampleActivity.kt
class TextSampleActivity : AppCompatActivity() {

    private val noInput = -2
    private val login = 1
    private val error = -1

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_text_sample)

        val actionBar = supportActionBar
        actionBar!!.setDisplayHomeAsUpEnabled(true)

        //yourNameを作成
        val name = generateUserName(10)
        userName.text = "あなたのユーザー名は${name}です\n長押しでコピーできます"

        loginButton.setOnClickListener { view ->
            val yourPassWord = editPassword.text.toString()
            val inputUserName = userNameInput.text.toString()
            val inputUserPassWord = userPassWordInput.text.toString()

            if (inputUserPassWord == ""){
                //yourPassWordの入力を促す
                showResult(view, noInput)
            }else{
                val loginResult = name == inputUserName && yourPassWord == inputUserPassWord

                if (loginResult){
                    //login
                    showResult(view, login)
                }else{
                    //error
                    showResult(view, error)
                }
            }

        }

        userNameInput.addTextChangedListener(object: TextWatcher {
            override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {}
            override fun afterTextChanged(editable: Editable) {}
            override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
                if (charSequence.length > nameTextInputLayout.counterMaxLength) {
                    nameTextInputLayout.error = "ユーザー名は10文字以内で入力してください"
                } else {
                    nameTextInputLayout.error = null
                }
            }
        })

    }

    //引数の値の桁数で乱数作成
    private fun generateUserName(length: Int): String {

        val letters = "abcdefghijklmnopqrstuvwxyz0123456789"

        var str = ""
        while (str.length < length) {
            str += letters[Random().nextInt(letters.length)]
        }

        return str

    }

    @SuppressLint("SimpleDateFormat")
    private fun showResult(view: View, result: Int){

        var showText = ""
        val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
        val date = Date(System.currentTimeMillis())

        when (result) {
            noInput -> {
                showText = "パスワードを設定してください"
            }
            login -> {
                showText = "${dateFormat.format(date)}にログインしました！"
            }
            error -> {
                showText = "入力されたユーザー名やパスワードが正しくありません"
            }
        }

        Snackbar.make(view, showText, Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()

    }
}
```

# 参考

TextInputLayoutについては、こちらの記事を元に学びました。
- [Android - TextInputLayoutでテキストフィールド実装](https://qiita.com/beaa/items/ba1742f47a51a27f019f)


# 次回予告

もうすでに何回も使ってますが、ToastやSnackBarについて。
もしかしたら省略して別のものについてやる...かも

決してネタ切れというわけではございませんよ

# あとがき
大学の後輩などにも見せたいですが、Qiitaユーザーの皆さんからも需要があればコメント頂けると嬉しいです
間違い等あれば修正リクエストの方して頂けると助かります。