20210101のAndroidに関する記事は3件です。

Flutter初心者に必要な最低限の知識

もくじ

  1. はじめに
  2. 作成したコードの紹介
  3. 基本構成
  4. 動的に表示を変更
  5. 画面遷移
  6. 抑えておくべき記法
  7. さいごに

はじめに

Javaの経験はありますが、flutterおよびdartは初心者の私がアプリ作成までに学んだ、最低限これだけは抑えておくべき内容を厳選してまとめておきます。
「初心者だけど、とにかく早くアプリが作成したい」「flutter始めたいけど調べるのが面倒くさい」なんて人にはおすすめできる内容になっていると思っています。
この記事が誰かのお役に立てれば幸いです。

flutterとは

作成したコードの紹介

sample1.jpg

sample2.jpg

作成してみたflutterアプリのmainファイルを最下部に記載しました。
動的な表示変更、画面遷移、ファイル読み込みを行っています。

基本構成

動かすために必要なクラスがあります。
下記の3つのクラスです。

メイン関数

void main()

ひとつめはmain関数。
プログラムが実行されると始めに実行される部分になります。

アプリケーションクラス

class MyApp extends StatelessWidget

2つ目はアプリケーションを定義するクラス。
このクラスでhome画面を指定したりします。

静的な画面クラス

class MenuPage extends StatelessWidget

3つ目は画面を定義するクラス。
上記は静的な画面の場合になります。

動的な画面クラス

class ScenarioPageLapper extends StatefulWidget

class _ScenarioPage extends State<ScenarioPageLapper>

動的な画面を使用する場合は上記2つのクラスが必要です。

動的に表示を変更

動的に表示内容を変更する場合は下記のように2箇所記述します。

  setState(() {
    this.text = "changed";
  });

1箇所目はsetStateメソッドです。
このメソッドを呼び出すと表示内容が変更されます。

  child: Text('$text'),

2箇所目は表示を変える部分です。
上記のように記載することで、動的に表示が変わります。

画面遷移

画面遷移させるには下記のように記載します。

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: "タイトル")),
);

上記はサブ画面を呼び出す例。
現在開いているページの上に新しいページを重ねるイメージです。

Navigator.pop(context);

上記はサブ画面を閉じる例。
主に戻る際の処理として利用します。
前述した通り、Navigator.pushを行うとページが重なっていくため、Navigator.pushのみで画面遷移を行うと何重にもページが重なってしまいます。

抑えておくべき記法

Java経験者の私が抑えておくと良いと思うdartの記法をまとめてみました。

セッターの省略

Sample(this.name)
Sample(String name) {
  this.name = name;
}

上2つのコードは結果は同じです。
上記のように記述することでセッターが省略できます。

記述の省略

State<StatefulWidget> createState() => _ScenarioPage(title);
State<StatefulWidget> createState() {
  return _ScenarioPage(title);
}

上2つのコードは結果は同じです。
上記のように記述することができます。

初期化子

ScenarioPageLapper({Key key, this.title}) : super(key: key);

初期化子。
上記のように記述することで、コンストラクタの先頭でsuper(key: key)が実行されます。

非同期の処理

ファイル読み込み処理などは非同期の処理を利用する方法が一般的なようです。
下記のような感じになります。
Flutterでは非同期処理は頻出のようです。(Androidアプリでもそうだった気もしますが)

var result = getFileText();
result.then((content) => this.sc = new Scenario(content));

Future<String> getFileText() {
  return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT);
}

ホットリロード機能

便利なホットリロード機能というものがあります。
デバッグしながらコードを修正してリアルタイムに反映させることができます。
Terminalで"flutter run"のコマンドを実行するのではなく、VSCodeであればF5キーのデバッグ機能で実行します。

さいごに

flutterを手っ取り早く開発したい人向けに、最低限のポイントを厳選して記載してみました。
この記事がお役に立てれば幸いです。

コード

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'util.dart';
import 'scenario.dart';
import 'const.dart';

/// ==================================================
/// メイン処理
/// ==================================================
void main() {
  runApp(MyApp());
}

/// ==================================================
/// アプリケーション定義
/// ==================================================
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: TITLE,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MenuPage(),
    );
  }
}

/// ==================================================
/// メニュー画面
/// ==================================================
class MenuPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 画像エリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの60%
              height: MediaQuery.of(context).size.height * (60 / 100),
              // ボーダー
              decoration: BoxDecoration(
                border: Border.all(color: Colors.black),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

            ),

            // ボタンエリア1
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの15%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // Episode1
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_1),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: SC_FILE_NAME_1)),
                      );
                    },
                  ),

                  // Episode2
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_2),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),

                  // Episode3
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_3),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                ]
              )
            ),

            // ボタンエリア2
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの15%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // Episode4
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_4),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),

                  // Episode5
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_5),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),

                  // Episode6
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_6),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                ]
              )
            ),
          ]
        )
      )
    );
  }
}

/// ==================================================
/// シナリオ画面ラッパー
/// ==================================================
class ScenarioPageLapper extends StatefulWidget {
  /// タイトル
  final String title;

  /// =========================
  /// コンストラクタ
  /// =========================
  ScenarioPageLapper({Key key, this.title}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ScenarioPage(title);
}

/// ==================================================
/// シナリオ画面
/// ==================================================
class _ScenarioPage extends State<ScenarioPageLapper> {
  /// シナリオ名
  String scName;

  /// シナリオ
  Scenario sc;
  /// 描画データ
  ViewData viewData;

  /// 画像URL
  String imgUrl;
  /// テキスト
  String text;

  /// =========================
  /// コンストラクタ
  /// =========================
  _ScenarioPage(String scName) {
    // シナリオ名を設定
    this.scName = scName;
    // 非同期でファイルからテキストを取得してシナリオを初期化
    var result = getFileText();
    result.then((content) => this.sc = new Scenario(content));
    // 初期表示画像
    this.imgUrl = IMG_ROOT_PATH + INIT_IMG_NAME;
    // 初期表示テキスト
    this.text = INIT_TEXT;
  }

  /// =========================
  /// 描画更新
  /// =========================
  void updateView() {
    setState(() {
      switch (this.viewData.dataType.toString()) {
        case SC_TYPE_IMG:
          this.imgUrl = this.viewData.value;
          break;
        case SC_TYPE_FADEIN:
          this.imgUrl = this.viewData.value;
          break;
        case SC_TYPE_CUTIN:
          this.imgUrl = this.viewData.value;
          break;
        default:
          this.text = this.viewData.value;

          // 音声が指定されていれば音声再生
          if (this.viewData.audio != "") {
            // this.player.play(this.viewData.audio);
          }
      }
    });
  }

  /// =========================
  /// ★★★非同期処理★★★
  /// ファイルからテキストを取得
  /// =========================
  Future<String> getFileText() {
    return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 画像エリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの60%
              height: MediaQuery.of(context).size.height * (70 / 100),
              // ボーダー
              decoration: BoxDecoration(
                border: Border.all(color: Colors.black),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),
              // 画像 動的に変更される
              child: Image.asset('$imgUrl'),

            ),

            // テキストエリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの20%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // デコレーション
              decoration: BoxDecoration(
                // ボーダー
                border: Border.all(color: Colors.black),
                // カラー
                color: HexColor(BGCOLOR_TEXT_EREA),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),
              // テキストエリア 動的に変更される
              child: Text('$text'),

            ),

            // ボタンエリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの10%
              height: MediaQuery.of(context).size.height * (10 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // 戻るボタン
                  RaisedButton(
                    child: const Text(TEXT_PREV_BTN),
                    color: HexColor(BGCOLOR_PREV_BTN),
                    shape: const StadiumBorder(),
                    onPressed: () {
                      // 前の描画データを取得
                      this.viewData = this.sc.getPrevViewData();
                      this.updateView();
                    },
                  ),

                  // メニューへ戻るボタン
                  RaisedButton(
                    child: const Text(TEXT_MENU_BTN),
                    color: HexColor(BGCOLOR_MENU_BTN),
                    textColor: Colors.black,
                    onPressed: () {
                      // メニュー画面を表示
                      Navigator.pop(context);
                    },
                  ),

                  // 進むボタン
                  RaisedButton(
                    child: const Text(TEXT_NEXT_BTN),
                    color: HexColor(BGCOLOR_NEXT_BTN),
                    shape: const StadiumBorder(),
                    onPressed: () {
                      // 次の描画データを取得
                      print(this.sc);
                      this.viewData = this.sc.getNextViewData();
                      this.updateView();
                    },
                  ),
                ]
              )
            ),
          ]
        )
      )
    );
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android / Kotlin】フォーム(EditText)入力の際、背景タップで表示キーボードを閉じる

はじめに

Androidアプリ開発(Kotlin)で学んだ内容を備忘録として残します!

どのような機能か?

場面
  • フォーム(EditText)にフォーカスが当たっている
  • 入力用キーボードが表示されている
動作

上記の状態で背景画面をタップするとキーボードを閉じる

というもの

実装

Activity

MainActivity.kt
// ~ 省略 ~

// Viewがタッチされる時に処理が走るメソッド
override fun onTouchEvent(event: MotionEvent?): Boolean {
    // InputMethodManager をキャストしながら取得
    val inputMethodManager: InputMethodManager = 
        getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

    // エルビス演算子でViewを取得できなければ return false
    // focusViewには入力しようとしているのEditTextが取得されるはず
    val focusView = currentFocus ?: return false

    // このメソッドでキーボードを閉じる
    inputMethodManager.hideSoftInputFromWindow(
        focusView.windowToken,
        InputMethodManager.HIDE_NOT_ALWAYS
    )

    return false
}

これだけ。

ちなみに

キーボードを閉じるメソッドはInputMethodManagerクラスではこのように定義されている。

InputMethodManager.java
public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
    return hideSoftInputFromWindow(windowToken, flags, null);
}

共通化したらこんな形になりそう(おまけ)

共通化ファイル

objectで定義

KeyboardUtils.kt
object KeyboardUtils {
    fun hideKeyboard(focusView: View) {
        val inputMethodManager: InputMethodManager =
            focusView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(
            focusView.windowToken,
            InputMethodManager.HIDE_NOT_ALWAYS
        )
    }
}

Activity

Activityで呼ぶ

MainActivity.kt
// ~ 省略 ~

override fun onTouchEvent(event: MotionEvent?): Boolean {
    val focusView = currentFocus ?: return false
    KeyboardUtils.hideKeyboard(focusView)

    return false
}

補足

Activityに実装していれば、関連するFragmentでも同様に動作するためFragmentには実装の必要なし

スペシャルサンクス(参考にさせていただいた資料)

最後に

簡単な短い記事でしたが、誤り、ご指摘などあればコメントいただければ幸いです!

ありがとうございました!!

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

【Android】PreferenceFragmentCompatでTwitter風の設定画面を作る

PreferenceFragmentCompat、とても便利ですが痒いところに手が届きませんよね。特にTwitter系の設定レイアウトはよく見ますが、標準では用意されていません。
ここでは設定画面のカスタマイズ方法について説明していきたいと思います。

完成図 参考レイアウト(Twitter)
IMG_20201231_122559.png

タイトルではTwitter風と言いましたがあまり似ていませんね

実装

Android Studioに用意されているテンプレートSetting Activityを元に作っていきます。

1. 土台となるコード

PreferenceFragmentCompatはxmlで背景色を指定する方法がありませんので、setBackgroundColorを使用して背景色を変更します。

SettingsFragment.kt
package com.sakusaku.test

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = super.onCreateView(inflater, container, savedInstanceState)
        // 背景色の設定
        view?.setBackgroundColor(Color.parseColor("#F5F5F5"))
        return view
    }
}

次に設定画面のレイアウトを作成します。

res/layout/root_preferences.xml
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <ListPreference
        android:defaultValue="online"
        android:entries="@array/entries_preference_status"
        android:entryValues="@array/values_preference_status"
        android:key="preference_status"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_status"
        app:useSimpleSummaryProvider="true" />

    <ListPreference
        android:defaultValue="short"
        android:entries="@array/entries_preference_scan_period"
        android:entryValues="@array/values_preference_scan_period"
        android:key="preference_scan_period"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_scan_period"
        app:useSimpleSummaryProvider="true" />

    <PreferenceScreen
        android:layout="@layout/preference_diver"
        android:selectable="false" />

    <PreferenceScreen
        android:key="preference_notice"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_notice" />

    <PreferenceScreen
        android:key="preference_about"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_about" />

    <PreferenceScreen
        android:key="preference_send_feedback"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_send_feedback" />

    <PreferenceScreen
        android:key="preference_privacy_policy"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_privacy_policy" />

    <PreferenceScreen
        android:key="preference_oss_license"
        android:layout="@layout/custom_preference"
        android:title="@string/preference_oss_license" />
</PreferenceScreen>

リソース等は下記の通りです。

res/values/string.xml
<resources>
    <string name="preference_status">ステータスを設定</string>
    <string name="preference_scan_period">スキャンの間隔</string>
    <string name="preference_notice">お知らせ</string>
    <string name="preference_about">このアプリについて</string>
    <string name="preference_send_feedback">フィードバックを送る</string>
    <string name="preference_privacy_policy">プライバシーポリシー</string>
    <string name="preference_oss_license">オープンソースライセンス</string>
</resources>
res/values/array.xml
<resources>
    <string-array name="entries_preference_status">
        <item>オンライン</item>
        <item>オフライン</item>
    </string-array>
    <string-array name="values_preference_status">
        <item>online</item>
        <item>offline</item>
    </string-array>
    <string-array name="entries_preference_scan_period">
        <item>短い</item>
        <item>中間</item>
        <item>長い</item>
    </string-array>
    <string-array name="values_preference_scan_period">
        <item>short</item>
        <item>middle</item>
        <item>long</item>
    </string-array>
</resources>

2. PreferenceScreenListPreference用のカスタムレイアウトを作成

Preferenceには標準で透過色が設定されているため、白の背景色を設定したカスタムレイアウトを作成します。
元のレイアウトはAOSP上に公開されています。

res/layout/custom_preference.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
          http://www.apache.org/licenses/LICENSE-2.0
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- Layout for a Preference in a PreferenceActivity. The
     Preference is able to place a specific widget for its particular
     type in the "widget_frame" layout. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:baselineAligned="false"
    android:clipToPadding="false"
    android:foreground="?attr/selectableItemBackground"
    android:gravity="center_vertical"
    android:minHeight="?android:attr/listPreferredItemHeightSmall"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">

    <LinearLayout
        android:id="@+android:id/icon_frame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="-4dp"
        android:gravity="start|center_vertical"
        android:minWidth="60dp"
        android:orientation="horizontal"
        android:paddingStart="0dp"
        android:paddingTop="4dp"
        android:paddingEnd="12dp"
        android:paddingBottom="4dp">

        <com.android.internal.widget.PreferenceImageView
            android:id="@+android:id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxWidth="48dp"
            android:maxHeight="48dp" />
    </LinearLayout>

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingTop="16dp"
        android:paddingBottom="16dp">

        <TextView
            android:id="@+android:id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceListItem" />

        <TextView
            android:id="@+android:id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+android:id/title"
            android:layout_alignStart="@+android:id/title"
            android:ellipsize="end"
            android:maxLines="10"
            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
            android:textColor="?android:attr/textColorSecondary" />
    </RelativeLayout>

    <LinearLayout
        android:id="@+android:id/widget_frame"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="end|center_vertical"
        android:orientation="vertical"
        android:paddingStart="16dp"
        android:paddingEnd="0dp" />
</LinearLayout>

3. Preference同士の隙間(区切り)を表示

Preferenceの区切りごとに隙間を開けるカスタムレイアウトを作成します。

res/layout/preference_diver.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="16dp" />
</LinearLayout>

表示の際はPreferenceScreenに上記のレイアウトを追加します。android:selectable="false"を指定することでクリックを無効化出来ます。

res/layout/custom_preference.xml
<PreferenceScreen
        android:layout="@layout/preference_diver"
        android:selectable="false" />

PreferenceScreenPreferenceCategoryに置き換えることで影をつけることも可能です。

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