Sibainu Relax Room

柴犬と過ごす

Android Studio でスマホアプリを作ってみた1

僕のこの顔がアイコンになったぞ。自慢げな顔をしている柴犬です。

概要

AndroidStudio を使ってスマホのアプリを作ってみようと思い立ち試行錯誤でやってみました。

ネットの情報を探して、何とか次のように読み込むことはできましたので記事にします。

ただし、仕組みの理解はできていません。あまり理解のないままで、ここまでできてしまうのは驚きです。

ちょっとハマりそうです。

緑色をバックに柴犬の顔をアイコンにしてみました。

今回お世話になりました本です。

次に紹介する本はちょっと過激な表紙ですが、本の内容はまじめてよく理解できる書き方をしています。先の「Androidアプリ開発」で飛ばしているところを丁寧に説明しているので、これで理解が早まりました。お勧めです。

しかもこれが100円で買え、ボリュームがすごい量です。なので著者に感謝です。

アクティビティのライフサイクルについて

アプリ作成のためには、ライフサイクルの理解が不可欠です。

次のDevelopers のドキュメントのガイドに解説があります。

https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja

そこにあるフローを日本語にしてみました。

このフローによると、下記の onCreate() に書いてある次の3行は、onResume() に書けばいいのかなと思っています。

ReadListener readlistener = new ReadListener();
ReadCode.setOnClickListener(readlistener);
CameraSetting();

長時間放置してスリープ状態になったとか、別のプログラムを起動して隠れたとかした場合、復帰する時すべて onResume() を通るからです。

今後、色々実験してみたいと考えています。

AndroidManifest.xml

アプリがネットを使用することを想定して追加しました。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

カメラを使うのと、縦固定にするために設定します。

android:hardwareAccelerated="true"

<activity>毎に挿入します
android:screenOrientation="portrait"

copy

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    // ネットに繋ぐとき必要
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        // カメラを使用します
        android:hardwareAccelerated="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.QRreader"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            //縦固定で使用します
            android:screenOrientation="portrait"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivty.java

copy

package heartbeat.systems.android.qrreader;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.DialogFragment;

import android.Manifest;
import android.app.Activity;
import android.app.Dialog;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.ResultPoint;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CompoundBarcodeView;
import com.journeyapps.barcodescanner.camera.CameraSettings;
import java.util.List;
//import android.util.Log;

public class MainActivity extends AppCompatActivity {

    CompoundBarcodeView barcodeView;
    private static String lastResult;
    private TextView NumberInput;
    private TextView NameInput;
    private ToneGenerator toneGenerator;
    private Button ReadCode;

    // アプリケーションの起動時の動作
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        NumberInput = findViewById(R.id.tvNumberInput);
        NameInput = findViewById(R.id.tvNameInput);
        ReadCode = findViewById(R.id.btRead);
        toneGenerator = new ToneGenerator(AudioManager.STREAM_SYSTEM, ToneGenerator.MAX_VOLUME);

        if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            String[] permissions = {Manifest.permission.CAMERA};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 100);
            return;
        }

        ReadListener readlistener = new ReadListener();
        ReadCode.setOnClickListener(readlistener);
        CameraSetting();
    }

    // 再スタートの動作
    @Override
    protected void onRestart() {
        super.onRestart();
        resetValue();
        CameraSetting();
        readBarcode();
    }

    // クリック時のリスナー
    private class ReadListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            resetValue();
            CameraSetting();
            readBarcode();
        }
    }

    // カメラの起動
    private void CameraSetting(){
        barcodeView = findViewById(R.id.barcodeView);
        CameraSettings settings = barcodeView.getBarcodeView().getCameraSettings();
        barcodeView.getBarcodeView().setCameraSettings(settings);
        barcodeView.setStatusText("バーコードが読めます");
        barcodeView.resume();
        readBarcode();
    }

    //バーコードの読み込み
    private void readBarcode(){
        barcodeView.decodeContinuous(new BarcodeCallback() {
            //final TextView NumberInput = findViewById(R.id.tvNumberInput);
            @Override
            public void barcodeResult(BarcodeResult result) {
                //このif文で、不必要な連続読みを防ぐ
                if (result.getText() == null || result.getText().equals(lastResult)){
                    return;
                }
                //既存表示が不用意に上書きされるのを防ぐ
                if (NumberInput.getText() != "") {
                    return;
                }
                //このif文で、読み取られたバーコードがJANコード QRコードかどうか判定する
                if ((result.getBarcodeFormat() != BarcodeFormat.EAN_13) &&
                        (result.getBarcodeFormat() != BarcodeFormat.QR_CODE)){
                    return;
                }
                //次が読み込まれるまで保持します
                lastResult = result.getText();
                //トーストに表示
                Toast.makeText(MainActivity.this, "読み取りました", Toast.LENGTH_SHORT).show();
                //画面に表示
                NumberInput.setText(result.getText());
                //ビープ音を鳴らしたいが鳴らない
                toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP);
                //ダイヤログを表示
                showDialog();
            }
            @Override
            public void possibleResultPoints(List<ResultPoint> resultPoints) {
            }
        });
    }

    // ダイヤログの表示
    void showDialog() {
        DialogFragment newFragment = StartDialogFragment.newInstance(
                R.string.alert_dialog_two_buttons_title);
        newFragment.show(getSupportFragmentManager(), "dialog");
    }

    // StartDialogFragment の作成
    public static class StartDialogFragment extends DialogFragment {
        public static StartDialogFragment newInstance(int title) {
            StartDialogFragment frag = new StartDialogFragment();
            Bundle args = new Bundle();
            args.putInt("title", title);
            frag.setArguments(args);
            return frag;
        }
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Activity activity=getActivity();
            if (activity==null) {
                return super.onCreateDialog(savedInstanceState);
            }

            TextView tvMessage = new TextView(getContext());
            tvMessage.setText(lastResult);
            tvMessage.setTextSize(15);
            // 文字色
            tvMessage.setTextColor( Color.YELLOW );
            // 背景色
            tvMessage.setBackgroundColor( Color.GRAY );
            // アイコンは適宜用意してその名前にします(ここではsibainu512.pngを使っています)
            return new AlertDialog.Builder(getActivity())
                    .setIcon(R.drawable.sibainu512)
                    .setTitle(R.string.dia_title)
                    .setView(tvMessage)
                    .setPositiveButton(R.string.dia_ok,
                            (dialogInterface, i) -> ((MainActivity) getActivity()).doPositiveClick())
                    .setNegativeButton(R.string.dia_cancel,
                            (dialogInterface, i) -> ((MainActivity) getActivity()).doNegativeClick())
                    .create();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState)
        {
            AlertDialog dialog = (AlertDialog)getDialog();
            dialog.setMessage("読み取った値を確認してください");
            return super.onCreateView(inflater, container, savedInstanceState);
        }
    }

    // ダイヤログの Positive button の click で実行される 
    public void doPositiveClick() {
        readBarcode();
        // Log.i("FragmentAlertDialog", "Positive click!");
    }

    // ダイヤログの Negative button の click で実行される 
    public void doNegativeClick() {
        readBarcode();
        // Log.i("FragmentAlertDialog", "Negative click!");
    }

    // デバイスの権限確認後、実行される動作
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 100 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
                return;
            }
        }

        ReadListener readlistener = new ReadListener();
        ReadCode.setOnClickListener(readlistener);
        CameraSetting();
    }

    // 値・表示の初期化
    public void resetValue(){
        NumberInput.setText("");
        NameInput.setText("");
        lastResult = null;
    }

}

activty_main.xml

垂直方向に並ぶようにしています。

copy

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.journeyapps.barcodescanner.CompoundBarcodeView
        android:id="@+id/barcodeView"
        android:layout_width="340dp"
        android:layout_height="340dp"
        android:layout_marginTop="40dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </com.journeyapps.barcodescanner.CompoundBarcodeView>

    <TextView
        android:id="@+id/tvNumberTelop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:text="@string/tv_numbertelop"
        android:textSize="15sp"
        app:layout_constraintTop_toBottomOf="@+id/barcodeView" />

    <TextView
        android:id="@+id/tvNumberInput"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:paddingStart="8dp"
        android:background="#77FF00"
        android:textSize="18sp"
        app:layout_constraintTop_toBottomOf="@+id/tvNumberTelop" />

    <TextView
        android:id="@+id/tvNameTelop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:text="@string/tv_nametelop"
        android:textSize="15sp"
        app:layout_constraintTop_toBottomOf="@+id/tvNumberInput" />

    <TextView
        android:id="@+id/tvNameInput"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:paddingStart="8dp"
        android:background="#77FF00"
        android:textSize="18sp"
        app:layout_constraintTop_toBottomOf="@+id/tvNameTelop" />

    <Button
        android:id="@+id/btRead"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:text="@string/bt_read"
        app:layout_constraintTop_toBottomOf="@+id/tvNameInput" />

</androidx.constraintlayout.widget.ConstraintLayout>

values/string.xml

表示文字、定数などの既定値をセットします。

copy

<resources>

    <string name="app_name">QrReader</string>
    <string name="tv_numbertelop">コード</string>
    <string name="tv_nametelop">名前</string>
    <string name="bt_read">読み込み</string>
    <string name="dia_ok">OK</string>
    <string name="dia_cancel">CANCEL</string>
    <string name="dia_title">確認</string>
    <string name="alert_dialog_two_buttons_title">2</string>

</resources>

build.gradle.kts (app)

compileSdk、targetSdk の設定値がまだ理解できていません。とにかくコンパイルが通るようにするため次の値にしました。

copy

plugins {
    id("com.android.application")
}

android {
    namespace = "heartbeat.systems.android.qrreader"
    // androidstudioの指示により 33 >> 34 に変更
    compileSdk = 34

    defaultConfig {
        applicationId = "heartbeat.systems.android.qrreader"
        minSdk = 26
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.10.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    // ZWing を使うためこの2行を追加します
    implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar")
    implementation("com.google.zxing:core:3.2.0")

}

sibainu512.png

ここでは、アイコンは「sibainu512.png」を使いましたが、これは適宜作成して次のフォルダーにコピーしてください。

C:\Users\(ユーザー名)\AndroidStudioProjects\(プロジェクト名)\app\src\main\res\drawable