20210809のAndroidに関する記事は4件です。

【SikuliX】スマホゲームの自動化(ウィンドウサイズ・位置固定)

はじめに 前回の記事では、スマホゲームを自動化するためのサンプルコードを記載しました。 今回はSikuliXを使用していて発生する問題の解決策を記載していきます。 発生する問題 SikuliXでは画面上の画像を認識してスクリプトにより操作を自動化します。 しかし、指定した画像サイズが画面上に表示されている画像サイズと違う場合、うまく画像認識されずスクリプトが正しく動作しない場合があります。 例えば、BlueStacksなどのエミュレータではウィンドウサイズを自由に変えることが可能です。 ウィンドウサイズ変更してしまい元のサイズに戻せなくなり、検索画像の取り直ししなければならない状況が発生することがあります。 この事象を解決するため、対象のウィンドウサイズを固定にする方法を検討しました。 サンプルコード setwindow.py #!/usr/bin/env python # -*- coding: utf-8 -*- import ctypes # ウィンドウハンドラ取得 hWnd = ctypes.windll.user32.FindWindowW(0, "BlueStacks") # 移動&リサイズ ctypes.windll.user32.MoveWindow(hWnd, 1200, 0, 700, 1100, 1) 解説 サンプルコードは今回もBlueStacksを使用した例です。 FindWindowWでウインドウ名「BlueStacks」のハンドラを取得し、そのハンドラに対してMoveWindowで(1200,0)の位置で700x1100のサイズに移動とリサイズをしています。 しかし、SikuliX2.0.4に組み込まれているPythonではWin32関連のモジュールは組み込まれていないようで、そのまま自動化のスクリプトに組み込んで実行するだけではエラーが発生します。 SikuliX上ではWin32関連モジュールは動作しない仕様のようなので、SikuliX上では動作させずウインドウサイズの変更はWindowsのコマンドラインから実行します。 SikuliXは、そのスクリプトを実行させることで実現します。 上記スクリプトを「C:\sikuli\setwindow.py」に保存したとします。 SikuliX上の別スクリプトから以下の記述で呼び出すことでsetwindow.pyが実行されます。 呼び出し元.py #ウィンドウサイズ固定 import subprocess subprocess.call('python C:\sikuli\setwindow.py') #以降は自動化スクリプトを記載 上記記述を自動化スクリプトの先頭に追加することで、ウィンドウサイズや位置を変更した場合でも、元の位置に戻すことが可能となります。 まとめ SikuliX外のPythonスクリプトをSikuliXから実行させることでWin32関連のウインドウサイズ変更を動作させるようにしました。 SikuliXを実行するとき、毎回固定したウインドウサイズにすることで画面サイズが変わることによる認識されない問題を回避できます。 自分の動作環境に応じて表示させる位置や画面サイズの数字を変更し使用してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらダークテーマ対応

最近になってようやくダークテーマ関連に足を踏み入れたのでまとめてみようと思いました。 呼び方 ・私も勘違いしていたのですが、Androidはダークモード・ライトモードではなくダークテーマ・ライトテーマと呼ぶそうですね。 参考:ダークテーマ 色分け ・ライトテーマ(デフォルト)は今まで通りvalues/colors.xmlに定義しておくとして、ダークテーマ用のものはどうすれば良いのでしょうか。答えは簡単で、values-night/colors.xmlを作成、その中に定義しておけばよさそうです。 ただし、ライトテーマ用のcolors.xmlに定義がないものをダークテーマ用のcolors.xmlに定義することはできないようです。あくまでオーバーライドです。 基準色 ・themeとして設定。以下あたりでしょうか。 Theme.AppCompat Theme.AppCompat.DayNight Theme.MaterialComponents.DayNight 任意のタイミングでテーマを切り替える ・最近はアプリ毎にテーマを設定できることがありますね。そのような場合はアプリ内設定画面などに適当なラジオボタンなどを置いてユーザの入力を読み取り、変更&保持を行えばよさそうです。 保持しているものを該当ActivityのonCreateで実行すると再度onCreateが走ってしまうので、もっと前段でテーマは決めてしまうのがよいですね。 (また、各Activityで受け取ることもできるようです。参考:ダークテーマ 構成の変更) 切り替えなどを行うUtilクラスがあると便利そうですね。ざっくりサンプルですが。 SampleThemeManager.kt object SampleThemeManager { /** * テーマを変更する(アプリ全体に反映). * @param mode MODE_NIGHT_YES, MODE_NIGHT_NO, MODE_NIGHT_FOLLOW_SYSTEM */ fun changeTheme(mode: Int) { // OS10以上の時だけ実行する if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return } // modeが妥当かのチェック処理 // 実際に変更する AppCompatDelegate.setDefaultNightMode(mode) // 設定された値を保持する処理 } /** * テーマを変更する(Activity単位で反映). * @param appCompatDelegate activityのdelegateなど * @param mode MODE_NIGHT_YES, MODE_NIGHT_NO, MODE_NIGHT_FOLLOW_SYSTEM */ fun changeTheme(appCompatDelegate: AppCompatDelegate, mode: Int) { // OS10以上の時だけ実行する if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return } // modeが妥当かのチェック処理 // 実際に変更する appCompatDelegate.localNightMode = mode // 設定された値を保持する処理 } /** * ダークテーマかどうか. * @param activity チェック対象 * @return trueならばダークテーマ */ fun isDarkTheme(activity: Activity): Boolean { return activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES } } 上記にあるように、適用範囲によって使い分ける必要があります。 アプリケーション全体に適用する Activityにだけ適用する AppCompatDelegate.setDefaultNightMode() appCompatDelegate.localNightMode 最後に ダークテーマはもはや無ければならないものになっているので、今後も知見を広げていきたいと思います。 リリース後の機能追加等でダークテーマを実装する場合はリソースの整理が大変になるので、できれば最初から要件には入れていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】UI テストに Page Object デザインパターンを適用する

Page Object デザインパターン UI テストにおける Page Object デザインパターンとは、1つの画面を1つのオブジェクトとして定義し、画面に対する操作や検証をオブジェクト経由で行う設計指針です。このデザインパターンを適用させることにより、可読性や保守性を向上させることができます。 コードを見た方がわかりやすいと思うので、今回は簡単な具体例で考えてみます。 ログイン処理の UI テスト Espresso を使った次のようなログイン処理の UI テストを例に考えます。 @Test fun loginSuccess() { // ユーザー ID を入力 onView(withId(R.id.user_id)).perform( ViewActions.replaceText("test_user_id") ) // パスワードを入力 onView(withId(R.id.password)).perform( ViewActions.replaceText("test_user_password") ) // ログインボタンをクリック onView(withId(R.id.login_button)).perform(click()) // ホーム画面に遷移して「ようこそ」が表示されていることを確認 onView(withId(R.id.welcome)).check( matches(withText("ようこそ")) ) } ログインに必要な情報を入力してログインをクリックし、次のページで「ようこそ」と表示されることを確認する UI テストです。 ここではログイン画面とログイン後表示されるホーム画面の2つの画面が登場します。この2つの画面について、それぞれ Page Object デザインパターンに沿ってオブジェクト化してみます。 ログイン画面のオブジェクト化 まずはログイン画面をオブジェクト化します。 Page Object デザインパターンに従って画面をオブジェクト化するときは「その画面でできること」を抽出して1つずつメソッド化します。今回のログイン画面で言えば下記のとおり。 ユーザー ID 欄にテキストを入力する パスワード欄にテキストを入力する ログインボタンを押してログインする この3つをオブジェクトに実装します。 class LoginPage() { fun inputUserId(userId: String): LoginPage { // ユーザー ID を入力 onView(withId(R.id.user_id)).perform( ViewActions.replaceText("test_user_id") ) return this } fun inputPassword(): LoginPage { // パスワードを入力 onView(withId(R.id.password)).perform( ViewActions.replaceText("test_user_password") ) return this } fun clickLoginButton(userId: String, password: String): HomePage { // ログインボタンをクリック onView(withId(R.id.login_button)).perform(click()) // 遷移先であるホーム画面のオブジェクトを返却する return HomePage() } } 3つのメソッドが実装できました。ここでポイントとなるのはメソッドの戻り値です。メソッドの戻り値は必ず Page Object にします。処理のあと画面に止まるのであれば this を、画面を遷移するのであれば遷移先のオブジェクトを返します。こうすることでメソッドチェーンが可能となり可読性が向上します。 LoginPage().inputUserId("...") .inputPassword("...") .clickLoginButton() ホーム画面のオブジェクト化 次にホーム画面をオブジェクト化します。 class HomePage() { fun assertWelcomeMessage(): HomePage { //「ようこそ」が表示されていることを確認 onView(withId(R.id.welcome)).check( matches(withText("ようこそ")) ) return this } } 今回は特にホーム画面で操作することがないので「ようこそ」が表示されていることを確認するアサーションだけ実装します。 完成形 下記がテストの完成形になります。 @Test fun loginSuccess() { LoginPage().inputUserId("test_user_id") .inputPassword("test_user_password") .clickLoginButton() .assertWelcomeMessage() } 一番最初に示したコードよりも、何をやっているか直感的でわかりやすくなっているかと思います。 また、誤ったパスワードが入力されたときにログインが失敗するテストを作りたい場合、inputPassword() に不正なパスワードを入力することでログインに失敗させることができます。ログイン成功、失敗それぞれのテストでメソッドを使いまわせるため、保守の側面においても効果的かと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

agora SDKを利用し、Android Studioで通話アプリを作成

Vcubeで通話処理をするためのサンプルアプリを作成したので、記録に残します。 動作環境 Android studio 4.1.2 Windows 10 Pro 20H2 agora consoleにてApp idを取得 agora console にアクセスする 会員登録を行い、consoleを表示する Project Managementをクリックし、Createをクリック プロジェクト名を入力し、認証方法をApp idのみを選択し「submit」をクリック プロジェクトが作成されるので、目のマークをクリックしapp idを取得する 実装 agora SDKを利用するための準備 ダウンロードページからAndroid VideoSDKをダウンロードし、libsフォルダをプロジェクト内にコピーする AndroidStudioでプロジェクトを開き、app/src/main/build.gradleファイルにsourceSetsとfileTreeを追記する app/build.gradle android { sourceSets{ main{ jniLibs.srcDirs = ['../../../libs'] } } } dependencies { implementation fileTree(dir: '../../../libs', includes: ['*.jar']) } カメラ/マイクの利用を許可する為、AndroidManifest.xmlを修正する app/AndroidManifest.xml <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.BLUETOOTH" /> VideoSDKをプロジェクトに統合する app/build.gradle allprojects { repositories { ... maven { url 'https://www.jitpack.io' } } } 画面の作成 UIの作成 activity_main.xml <?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"> <FrameLayout android:id="@+id/local_video_view_container" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" /> <FrameLayout android:id="@+id/remote_video_view_container" android:layout_width="160dp" android:layout_height="160dp" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:layout_marginTop="16dp" android:background="@android:color/darker_gray" /> </androidx.constraintlayout.widget.ConstraintLayout> 機能の作成 Androidクラスをインポート MainActivity.kt import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.pm.PackageManager; import android.view.SurfaceView; import android.widget.FrameLayout; パーミッションの設定 MainActivity.kt private val PERMISSION_REQ_ID_RECORD_AUDIO = 22 private val PERMISSION_REQ_ID_CAMERA = PERMISSION_REQ_ID_RECORD_AUDIO + 1 private fun checkSelfPermission(permission: String, requestCode: Int): Boolean { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode) return false } return true } Agoraクラスをインポートします。 MainActivity.kt import io.agora.rtc.IRtcEngineEventHandler; import io.agora.rtc.RtcEngine; import io.agora.rtc.video.VideoCanvas; ビデオハングアウトチャネルのコールバックを作成 MainActivity.kt /Fill the App ID of your project generated on Agora Console./ private val APP_ID = "取得したapp id" /Fill the channel name./ private val CHANNEL = "任意のチャンネル名" /Fill the temp token generated on Agora Console./ private val TOKEN = "" private var mRtcEngine: RtcEngine ?= null private val mRtcEventHandler = object : IRtcEngineEventHandler() { / Listen for the remote user joining the channel to get the uid of the user. / override fun onUserJoined(uid: Int, elapsed: Int) { runOnUiThread { / Call setupRemoteVideo to set the remote video view after getting uid from the onUserJoined callback. / setupRemoteVideo(uid) } } } チャネルに参加するためのコアメソッドを呼び出しを作成 MainActivity.kt private fun initializeAndJoinChannel() { try { mRtcEngine = RtcEngine.create(baseContext, APP_ID, mRtcEventHandler) } catch (e: Exception) { } / By default, video is disabled, and you need to call enableVideo to start a video stream. / mRtcEngine!!.enableVideo() val localContainer = findViewById(R.id.local_video_view_container) as FrameLayout / Call CreateRendererView to create a SurfaceView object and add it as a child to the FrameLayout. / val localFrame = RtcEngine.CreateRendererView(baseContext) localContainer.addView(localFrame) / Pass the SurfaceView object to Agora so that it renders the local video. / mRtcEngine!!.setupLocalVideo(VideoCanvas(localFrame, VideoCanvas.RENDER_MODE_FIT, 0)) / Join the channel with a token. / mRtcEngine!!.joinChannel(TOKEN, CHANNEL, "", 0) } リモートユーザーがチャネルに参加するときに、リモートインターフェイスを追加する MainActivity.kt private fun setupRemoteVideo(uid: Int) { val remoteContainer = findViewById(R.id.remote_video_view_container) as FrameLayout val remoteFrame = RtcEngine.CreateRendererView(baseContext) remoteFrame.setZOrderMediaOverlay(true) remoteContainer.addView(remoteFrame) mRtcEngine!!.setupRemoteVideo(VideoCanvas(remoteFrame, VideoCanvas.RENDER_MODE_FIT, uid)) } アプリを開始処理を作成 MainActivity.kt override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // If all the permissions are granted, initialize the RtcEngine object and join a channel. if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO) && checkSelfPermission( Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA )) { initializeAndJoinChannel() } } アプリを閉じたら通話を終了するように作成 MainActivity.kt override fun onDestroy() { super.onDestroy() mRtcEngine?.leaveChannel() RtcEngine.destroy() } 参照URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む