- 投稿日:2020-12-12T23:47:47+09:00
Android studio:Kotlin:shared preferenceの使い方
アプリ版のini設定みたいなやつ
とりあえずこれを書く
MainActivity.kt//第一引数に名前を入れとくとアプリ全体で共有できる val spf = getSharedPreferences("Setting", MODE_PRIVATE) val editor = spf.edit()読み込みはこう(デフォルト値は必須)
MainActivity.ktspf.getInt("Key", "ここにデフォルトの値を入れる") spf.getString("Key", "ここにデフォルトの値を入れる") spf.getFloat("Key", "ここにデフォルトの値を入れる") //こんな感じで使う var A=spf.getInt("Key", "ここにデフォルトの値を入れる")書き込みはこう
MainActivity.kteditor.putInt("Key",1) editor.putString("Key","内容") editor.putFloat("Key",1F) //applyしたら書き込まれる editor.apply()一度全削除してインストール直後の状態を試したい場合は以下
MainActivity.ktval spf = getSharedPreferences("Setting", MODE_PRIVATE) val editor = spf.edit() editor.clear() editor.apply()
- 投稿日:2020-12-12T23:39:19+09:00
【Android】備忘録: 権限リクエストのやり方
【Android】備忘録: 権限リクエストの方法
概要
完全な自分用の備忘録です。
毎回、パーミションのリクエストの仕方を忘れるので、その記録です。手順
- manifestに、権限の宣言を追加する。
- 実行時に、permissionの状態を確認する
- 許可されていなければ、リクエストを行う
- リクエストの結果で処理を実行
1. manifestに、権限の宣言を追加する。
AndroidManifest.xmlに、権限についての宣言を行う。
- 例: カメラを使いたい時
<users-permission android:name="~"/>を追加AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.sample.sample"> // この部分が必要 <uses-permission android:name="android.permission.CAMERA" /> <application ~~~省略~~~ > </aplication> </manifest>2. 実行時に、permissionの状態を確認する
例えば、buttonをタップした時に、permissionの確認をする場合
- 例: cameraButtonをタップした時に、確認
注意するところ
- Manifestが、java.util.Manifestをimportしないこと。android.Manifestをimportする。
~Activity.ktval cameraButton = binding.camera cameraButton.setOnClickListener { // この部分が必要 // checkSelfPermissionで、Permissionの状態を確認する // != PackageManager.PERMISSION_GRANTEDなので、許可されていないという条件分岐 if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // この部分は、「3. 許可されていなければ、リクエストを行う」で追加 } else { // 許可されていれば、カメラ起動 startActivity(Intent.(MediaStore.ACTION_IMAGE_CAPTURE)) } }3. 許可されていなければ、リクエストを行う
- 2.で書いたコードに、リクエストの処理を追加
~Activity.ktcameraButton.setOnClickListener { // != PackageManager.PERMISSION_GRANTEDなので、許可されていないという条件分岐 if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // この部分が、追加 // 最後のREQUEST_CAMERA_PERMISSIONは、任意の定数 ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION) } else { // 許可されていれば、カメラ起動 startActivity(Intent.(MediaStore.ACTION_IMAGE_CAPTURE)) } }4. リクエストの結果で処理を実行
- permissionのリクエストの結果は、onRequestPermissionsResultで受け取る
~Activity.ktoverride fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { if (requestCode == REQUEST_CAMERA_PERMISSION) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // カメラ起動 startActivity(Intent.(MediaStore.ACTION_IMAGE_CAPTURE)) } super.onRequestPermissionsResult(requestCode, permissions, grantResults) }
- 投稿日:2020-12-12T23:29:19+09:00
Dromiを使いProxy下のAndroid端末でLINEを使う
Dromiとは
HTTP, Socks ProxyをVPNとして接続するアプリです。
https://play.google.com/store/apps/details?id=org.sandrob.drony&hl=ja&gl=US環境
- 認証付きProxy必須環境
- 22番ポートはProxyなしで通信可能
- AzureにSquidでHTTP proxyを構築済み
叩くコマンド
# 外のProxyサーバで行う /etc/ssh/sshd_configの GatewayPorts をyesにする # APにするPCで行う ssh -gNfL 0.0.0.0:8888:localhost:8080 username@ipadress sudo create_ap [無線LANデバイス名] [共有元デバイス名] --no-dnsmasq -d --ieee80211n --redirect-to-localhost [SSID] [パスワード] & ip a (ap0のIPアドレスを確認する)主な流れ
ssh -gNfL 0.0.0.0:8888:localhost:8080 username@ipadressで0.0.0.0へAzureのHTTP proxyを持ってくるcreate_apを活用して無線APを建てる- スマホでWi-Fiに接続する このとき,Proxyにはスマホ側のデフォルトゲートウェイをアドレスに設定します。
- Deoniで先程と同様にProxyの設定をする
create_apについて
実行してできる仮想ネットワークデバイスのIPアドレスは固定になります。
それがスマホ側のデフォルトゲートウェイになります。Deoniの設定
google, ブラウザ, Twitterなど元からHTTP proxyに対応しているアプリは
Direct設定にすると正常に通信可能です。
LINE, Discord, InstagramなどHTTP proxyに非対応のアプリはVPN経由で通信しようとし,結果的にProxyを通せます。犠牲にしたもの
VPNに繋がってしまう関係でKDE Connectが死にました。
- 投稿日:2020-12-12T23:29:19+09:00
Dronyを使いProxy下のAndroid端末でLINEを使う
Dronyとは
HTTP, Socks ProxyをVPNとして接続するアプリです。
https://play.google.com/store/apps/details?id=org.sandrob.drony&hl=ja&gl=US環境
- 認証付きProxy必須環境
- 22番ポートはProxyなしで通信可能
- AzureにSquidでHTTP proxyを構築済み
叩くコマンド
# 外のProxyサーバで行う /etc/ssh/sshd_configの GatewayPorts をyesにする # APにするPCで行う ssh -gNfL 0.0.0.0:8888:localhost:8080 username@ipadress sudo create_ap [無線LANデバイス名] [共有元デバイス名] --no-dnsmasq -d --ieee80211n --redirect-to-localhost [SSID] [パスワード] & ip a (ap0のIPアドレスを確認する)主な流れ
ssh -gNfL 0.0.0.0:8888:localhost:8080 username@ipadressで0.0.0.0へAzureのHTTP proxyを持ってくるcreate_apを活用して無線APを建てる- スマホでWi-Fiに接続する このとき,Proxyにはスマホ側のデフォルトゲートウェイをアドレスに設定します。
- Deoniで先程と同様にProxyの設定をする
create_apについて
実行してできる仮想ネットワークデバイスのIPアドレスは固定になります。
それがスマホ側のデフォルトゲートウェイになります。Dronyの設定
google, ブラウザ, Twitterなど元からHTTP proxyに対応しているアプリは
Direct設定にすると正常に通信可能です。
LINE, Discord, InstagramなどHTTP proxyに非対応のアプリはVPN経由で通信しようとし,結果的にProxyを通せます。犠牲にしたもの
VPNに繋がってしまう関係でKDE Connectが死にました。
- 投稿日:2020-12-12T20:20:43+09:00
Androidでもリアルタイムにブラーをかける
序
最近はWebページでもブラー(ぼかし)エフェクトを使用したサイトがみられるようになりましたね(今からだと旬は過ぎた感ありますが…)。
iOSだと6年前のiOS 8からUIVisualEffectViewという便利なビューが標準で用意されたので、ミニマムだとレイアウトだけで実現できてしまいます。Layout風に書くとこんな感じ…
<UIView> <!-- UIViewはViewGroupのように子ビューを複数持てる --> <UIScrollView> <UIImageView> </UIScrollView> <UIVisualEffectView /> <!-- Androidと同じく下に書くと階層的には上に来て下にあるビューにブラーがかかる --> </UIView>最近Webでも
backdrop-filterによってIEを除いた最新のブラウザで使えるようになりました:雑ですが iOS のサンプルを大まかに再現するとこうなります:
ちょっと長いので省略
<html> <head> <style type="text/css"> img { object-fit: cover; width: 100%; height: 200vh; } .overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; } .box { margin: auto; width: 75vw; height: 75vw; backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); /* Safari */ background-color: #ffffff5f; } </style> </head> <body> <img src="..."> <div class="overlay"> <div class="box"></div> </div> </body> </html>本題
画像やビューのキャプチャに対してブラーをかける方法についてはすでに 記事
や 質問 がありましたが、スクロールなどリアルタイムにかける方法についてはまだ情報がまとまっていなさそうだったので、実現するライブラリについていろいろ書いていきたいと思います。ちなみに
- 今回のような例であれば上記ライブラリであらかじめビットマップを作っておいてスクロールに合わせて動かせば擬似的に見せることは可能でした(発想の転換)
- 静止画に対するブラーの掛け方は RenderScript から CPU で Bitmap を直接触る方法などいろいろありますが、興味深いことに StackOverflow でパフォーマンスを測定した方がおられたので共有しておきます
RealtimeBlurView
ずばりな名前のライブラリでレイアウトに乗っけるだけで背景のビューにブラーがかかってくれるすごいライブラリです。説明に書いてある通りにするだけで確かに実現ができました:
サンプルコードはこちらです: https://github.com/cubenoy22/android-reatime-blur
どうやって実現しているのか
ただライブラリを紹介するだけではつまらないのでちょっと中身をみていきたいと思います。
RealtimeBlurView
XMLで配置するビューの実装から見ていきます。対象ファイルは こちら。
ブラー用のビットマップ作成
prepare() メソッドでメンバーの mBitmapToBlur をビューの大きさ分で作成していて、描画用の
mBlurringCanvasも同時に作成しています。このメソッドは後ほど紹介する onPreDraw 内で呼ばれる ようになっていて、画面の更新がされる直前かつまだ準備できていなければ実行されるようになっています。ビューのサイズ変更もここで対応できるようになっていました。protected boolean prepare() { // 省略 int scaledWidth = Math.max(1, (int) (width / downsampleFactor)); int scaledHeight = Math.max(1, (int) (height / downsampleFactor)); if (mBlurringCanvas == null || mBlurredBitmap == null || mBlurredBitmap.getWidth() != scaledWidth || mBlurredBitmap.getHeight() != scaledHeight) { mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); } mBlurringCanvas = new Canvas(mBitmapToBlur); mBlurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); }ブラーが必要なエリアをビットマップに転写
Activity の Window の decorView に対して
- ViewTreeObserver.OnPreDrawListener をセットし画面の更新を受け取れるように
L301
- background があれば上で用意した
mBitmapToBlurへ描画L263
- 一番肝心な draw() をコールし
mBitmapToBlurへ描画L265
private final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { // 省略 if (decor != null && isShown() && prepare()) { if (decor.getBackground() != null) { decor.getBackground().draw(mBlurringCanvas); } decor.draw(mBlurringCanvas); blur(mBitmapToBlur, mBlurredBitmap); } } }これであらかじめ作っておいた Bitmap (mBitmapToBlur) に対してビューの内容が転写されました。面白い。
転写した Bitmap に対してブラーをかける
直前の preDrawListener での一連の流れでブラーもかけています。
mBitmapToBlurの他にもう一つmBlurredBitmapという Bitmap が用意されています。このビットマップは実はmBitmapToBlurと同時に同じサイズで用意 されていました。上のコードにも書いておきましたが blur() 関数ですね。protected void blur(Bitmap bitmapToBlur, Bitmap blurredBitmap) { mBlurImpl.blur(bitmapToBlur, blurredBitmap); }mBlurImpl ( BlurImpl ) は getBlurImpl メソッドで状況に応じた実装が使用されるようになっており、違いは RenderScript のパッケージが各種用意されているだけのようでした:
ScriptIntrinsicBlur を使って
mBitmapToBlurの内容にブラーをmBlurrerdBitmapに出力させていることがわかります。@Override public void blur(Bitmap input, Bitmap output) { mBlurInput.copyFrom(input); mBlurScript.setInput(mBlurInput); mBlurScript.forEach(mBlurOutput); mBlurOutput.copyTo(output); }これで表示するための準備ができあがりました。
onDraw で mBlurredBitmap を描画する
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBlurredBitmap(canvas, mBlurredBitmap, mOverlayColor); }ブラーをかけた Bitmap を表示すれば完了となるわけですね!
BlurKit
こちらの方がスター数が多いですが
- onStart / onStop にコードを書かないと動かない
- 毎フレーム必ずinvalidate()をコールしていて負荷が高そう
BlurLayout.java#L111
- 毎フレームごとに Bitmap を作成していて効率が悪そう
BlurLayout.java#L164, BlurLayout.java#L249
という理由から個人的に RealtimeBlurView をオススメしたいです。ちなみにブラーをかけるところは ほぼ同じ でビットマップをひとつだけしか使用しないか二つ使用するかが唯一異なる点でした(RealtimeBlurViewのビットマップが二つ必要な理由についてはまだ調査していません)。
(ちょこっと追記) ほぼ同じって書きましたが RenderScript のコードも改めてみてみると毎回初期化していてここも効率化できそう。。
BlurView
こちらはレイアウト以外にコードを書く必要があるみたいです。コードは読みやすい気がします。
- ブラーをかける制御
BlockingBlurController.java
- ブラーをかける処理
RenderScriptBlur.java
全体的に RealtimeBlurView と同じような実装で設計と RenderScript のビットマップを2つ使用するかどうかの違いしかなさそうでした。気軽に使用できる点からやはり RealtimeBlurView をおすすめしたいです。RealtimeBlurViewのビットマップが二重になっているところが改善されたら今のところ最強でしょうかね(プルリク出してみようかしら…)。
終わりに
Androidのライブラリ3つを紹介しましたがいずれもビットマップを用意したりしています。おそらくiOSやWebではGPUのレンダリングパイプラインあたりだけで実現している気がしていて、Androidでもそういった高効率のアプローチができないか調査する余地がありそう、という感想を持ちました。できればGoogle純正のイケてるライブラリが出てくれると嬉しかったり。
- 投稿日:2020-12-12T17:18:31+09:00
Xamarin.FormsでAndroidのデバイスの設定の影響でアプリのフォントサイズが変わらないようにする方法
はじめに
Androidは、下図のような画面で、デバイスのフォントサイズを変更できます。
これは、アプリのフォントサイズに影響するため、普通にXamarin.Formsでアプリを開発すると、フォントサイズがこの設定の影響で変わってしまいます。それを変わらないようにする方法の紹介です。
実装方法
Androidプロジェクトにある MainActivity.cs に以下のコードブロックを追加することで、デバイスのフォントサイズに影響しなくなります。
MainActivity.cs public override Android.Content.Res.Resources Resources { get { Android.Content.Res.Resources res = base.Resources; Configuration config = new Configuration(); config.SetToDefaults(); res?.UpdateConfiguration(config, res.DisplayMetrics); return res; } }ただし、上記で利用している Resources.UpdateConfigurationメソッドは、Android API 25(Android7.1)以上では、非推奨となっています。
詳しくは、以下に書いてあります。
Resources | Android Developers従って、Android API 25(Android7.1)以降と、それより前で分岐して、別々の方法で実装する必要があります(自分で確認した限り、新しい方式の実装は、Android API 24(Android7.0)では、正しく動作しませんでした)。
public override Android.Content.Res.Resources Resources { get { if (DeviceInfo.Version.Major < 7 || (DeviceInfo.Version.Major == 7 && DeviceInfo.Version.Minor == 0)) { // Android API 24(Android7.0)以前は古い方式で実装 Android.Content.Res.Resources res = base.Resources; Configuration config = new Configuration(); config.SetToDefaults(); res?.UpdateConfiguration(config, res.DisplayMetrics); return res; } else { // Android API 25(Android7.1)以降は新しい方式で実装 var config = new Configuration(); config.SetToDefaults(); return CreateConfigurationContext(config)?.Resources; } } }古い方式と新しい方式のそれぞれの実装方法は、以下の stack overflow の記事を参考にしています。
How to Prevent Device font size effect of Xamarin android app?まとめ
Xamarin.FormsはC#でWindowsとAndroidとiOSのアプリがまとめて開発できて便利です。
ちなみに私は、普段はエンジニアリングマネージャーとして、チームの皆で楽しく開発する施策を色々実施しています。詳しくは以下を参照ください。
1年以上かけて生産性倍増+成長し続けるチームになった施策を全部公開Twitterでも開発に役立つ情報を発信しています → @kojimadev
- 投稿日:2020-12-12T16:57:51+09:00
【Flutter】遭遇したエラー&&解決策まとめ
参考文献
- Xcode12にアップデートしたらPodライブラリで大量に警告が発生したので対処した件
- FlutterでGoogleDataTransportにエラーが出た時の対処
- 【Flutter】エラー「firebase_core_web not found.」の対策
- 【Flutter】No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()の対処法
Podライブラリの警告
◆ エラー内容
The iOS Simulator deployment target ‘IPHONEOS_DEPLOYMENT_TARGET’ is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.◆ 解決策
Podfilepost_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' end end end$ pod updateGoogleDataTransportエラー
◆ エラー内容
error: umbrella header for module 'GoogleDataTransport' does not include header 'GDTCORDataFuture.h'◆ 解決策
$ rm -rf Pods $ flutter clean $ flutter run //pod installが自動実行される「firebase_core_web not found」エラー
◆ エラー内容
Plugin project :firebase_core_web not found. Please update settings.gradle.◆ 解決策
setting.gradledef flutterProjectRoot = rootProject.projectDir.parentFile.toPath() def plugins = new Properties() def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } } plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() include ":$name" project(":$name").projectDir = pluginDirectory }「Firebase.initializeApp」エラー
◆ エラー内容
No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()◆ 解決策
Widget _buildBody(BuildContext context) { Firebase.initializeApp(); // new
- 投稿日:2020-12-12T12:10:12+09:00
PCでAndoroidを動かす【草案中】
- 投稿日:2020-12-12T10:56:50+09:00
【Ktolin初心者】ProgressBarを実装してみた
はじめに
よくある画面読み込み中にあるリクエストAPIクリエストの処理待ちに使えわれるProgressBarについて解説してみます。
この記事書いてる人
株式会社evoluのAndroidエンジニア(3ヶ月目)
イメージ
progress barの実装デモ画面になります
こんな感じで読み込みが終わると次の画面が表示されます
コード
MainActivity.ktpackage com.example.progressbar import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.ProgressBar class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val progress = findViewById<ProgressBar>(R.id.progress_bar) } }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"> <ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>これでひとまずActivityにProgressBarを表示することができます
- 投稿日:2020-12-12T10:43:56+09:00
【Flutter】 iphoneのようにボトムナビゲーションを作る
Flutterでの下タブでのナビゲーションについてです。
マテリアルデザインであればそのままBottomNavigationBarを使用すればいいのですがその場合
画面遷移で下タブが消えてしまうので遷移後に別タブの画面切り替えができません。今回の記事はiosのように下タブを残したまま画面遷移をさせる方法についてです。
以下を記述しています。
- CupertinoTabScaffoldを使用しながら下タブを表示させずに画面遷移する方法
- androidのバックキーを押下した際に現在のタブから前画面に戻る方法
CupertinoTabScaffoldを使用しながら下タブを表示させずに画面遷移する方法
画面遷移する際にrootNavigatorをtrueにしてpushします。
Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (context) => nextPage(), ), );androidのバックキー押下した際に現在のタブから前画面に戻る方法
上記のpushのように下タブを表示せずに画面遷移した場合は問題ありませんが
CupertinoTabScaffoldを使用して下タブを表示しながら画面遷移をした場合androidでバックキーを押すとアプリが閉じます。
これはバックキーでpopの処理が行われているのですが
表示されている画面のcontextではなく、CupertinoTabScaffoldを使用した親画面のcontextを用いて
popしているため親画面は画面遷移していないのでアプリが閉じています。
※iOSの場合は特に何もしなくても画面を左スワイプで前画面に戻ることが可能です。方法としてはstaticで各タブのcontextを保持しておき、バックキーを押されたタイミングで親画面から各タブのcontextを呼び出しpopします。
/// androidのバックキー制御するための各タブ分のcontext保持クラス class ConstantsChildContext { static int selectedIndex = 0; static BuildContext childContext0; static BuildContext childContext1; static BuildContext childContext2; }各タブのbuild時にcontextを初回だけセットします。
/// 初回に各タブのChildContextをセットする void setChildContext({@required BuildContext childContext}) { switch (ConstantsChildContext.selectedIndex) { case 0: if (ConstantsChildContext.childContext0 == null) { ConstantsChildContext.childContext0 = childContext; } break; case 1: if (ConstantsChildContext.childContext1 == null) { ConstantsChildContext.childContext1 = childContext; } break; case 2: if (ConstantsChildContext.childContext2 == null) { ConstantsChildContext.childContext2 = childContext; } break; } }最後に親画面でCupertinoTabScaffoldの親WidgetとしてWillPopScopeを書き、
ここでバックキーを押した際のイベントを取得します。@override Widget build(BuildContext context) { /// WillPopScopeで親画面のbackKeyイベントを取得し、現在のタブのcontextをpopして前画面に戻る return WillPopScope( onWillPop: () async { /// バックキー押下時のイベント取得 return _onBackKeyAndroid(); }, child: CupertinoTabScaffold(), ); }各タブで前画面に戻れるかどうかを判定しbool値をかえします。
/// バックキーをタップすると各画面のpopを実行(Androidのみ) bool _onBackKeyAndroid() { if (Platform.isAndroid) { switch (ConstantsChildContext.selectedIndex) { case 0: if (Navigator.canPop(ConstantsChildContext.childContext0)) { Navigator.pop(ConstantsChildContext.childContext0); } else { /// 前画面に戻れない場合にアプリを閉じたくなければここはfalse return true; } break; case 1: if (Navigator.canPop(ConstantsChildContext.childContext1)) { Navigator.pop(ConstantsChildContext.childContext1); } else { return true; } break; case 2: if (Navigator.canPop(ConstantsChildContext.childContext2)) { Navigator.pop(ConstantsChildContext.childContext2); } else { return true; } break; } } return false; }これでandroidのバックキー制御ができるようになりました。
こちらで勉強しました。
サンプルコードのデザインはそのままです。サンプルコード
main.dartimport 'package:flutter/material.dart'; import 'home_page.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: HomePage(), ); } }home_page.dartimport 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'constants_child_context.dart'; import 'custom_page.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { static List<Widget> _pageList = [ CustomPage( pannelColor: Colors.cyan, title: 'Home', pageCount: 1, ), CustomPage( pannelColor: Colors.green, title: 'Settings', pageCount: 1, ), CustomPage( pannelColor: Colors.pink, title: 'Search', pageCount: 1, ) ]; void _onItemTapped(int index) { setState(() { ConstantsChildContext.selectedIndex = index; }); } /// バックキーをタップすると各画面のpopを実行(Androidのみ) bool _onBackKeyAndroid() { if (Platform.isAndroid) { switch (ConstantsChildContext.selectedIndex) { case 0: if (Navigator.canPop(ConstantsChildContext.childContext0)) { Navigator.pop(ConstantsChildContext.childContext0); } else { /// 前画面に戻れない場合にアプリを閉じたくなければここはfalse return true; } break; case 1: if (Navigator.canPop(ConstantsChildContext.childContext1)) { Navigator.pop(ConstantsChildContext.childContext1); } else { return true; } break; case 2: if (Navigator.canPop(ConstantsChildContext.childContext2)) { Navigator.pop(ConstantsChildContext.childContext2); } else { return true; } break; } } return false; } @override Widget build(BuildContext context) { /// WillPopScopeで親画面のbackKeyイベントを取得し、現在のタブのcontextをpopして前画面に戻る return WillPopScope( onWillPop: () async { return _onBackKeyAndroid(); }, child: CupertinoTabScaffold( tabBar: CupertinoTabBar( items: [ BottomNavigationBarItem( icon: Icon(Icons.home), ), BottomNavigationBarItem( icon: Icon(Icons.settings), ), BottomNavigationBarItem( icon: Icon(Icons.search), ), ], currentIndex: ConstantsChildContext.selectedIndex, onTap: _onItemTapped, backgroundColor: Colors.white, ), tabBuilder: (context, index) { return CupertinoTabView( builder: (context) { return _pageList[index]; }, ); }, ), ); } }constants_child_context.dartimport 'package:flutter/cupertino.dart'; /// androidのバックキー制御するための各タブ分のcontext保持クラス class ConstantsChildContext { static int selectedIndex = 0; static BuildContext childContext0; static BuildContext childContext1; static BuildContext childContext2; } /// 初回に各タブのChildContextをセットする void setChildContext({@required BuildContext childContext}) { switch (ConstantsChildContext.selectedIndex) { case 0: if (ConstantsChildContext.childContext0 == null) { ConstantsChildContext.childContext0 = childContext; } break; case 1: if (ConstantsChildContext.childContext1 == null) { ConstantsChildContext.childContext1 = childContext; } break; case 2: if (ConstantsChildContext.childContext2 == null) { ConstantsChildContext.childContext2 = childContext; } break; } }custom_page.dartimport 'package:flutter/material.dart'; import 'constants_child_context.dart'; import 'full_screen_custom_page.dart'; class CustomPage extends StatelessWidget { final Color pannelColor; final String title; final int pageCount; CustomPage( {@required this.pannelColor, @required this.title, @required this.pageCount}); @override Widget build(BuildContext context) { setChildContext(childContext: context); final titleTextStyle = Theme.of(context).textTheme.title; return Scaffold( appBar: AppBar(), body: Container( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 200, height: 200, decoration: BoxDecoration( color: pannelColor, borderRadius: BorderRadius.all(Radius.circular(20.0))), child: Center( child: Text( title + pageCount.toString(), style: TextStyle( fontSize: titleTextStyle.fontSize, color: titleTextStyle.color, ), ), ), ), TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => CustomPage( pannelColor: pannelColor, title: title, pageCount: pageCount + 1, ), ), ); }, child: Text('下タブあり次画面')), TextButton( onPressed: () { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (context) => FullScreenCustomPage( pannelColor: pannelColor, title: title, pageCount: pageCount + 1), ), ); }, child: Text('下タブなし次画面')), ], ), ), ), ); } }full_screen_custom_page.dartimport 'package:flutter/material.dart'; class FullScreenCustomPage extends StatelessWidget { final Color pannelColor; final String title; final int pageCount; FullScreenCustomPage( {@required this.pannelColor, @required this.title, @required this.pageCount}); @override Widget build(BuildContext context) { final titleTextStyle = Theme.of(context).textTheme.title; return Scaffold( appBar: AppBar(), body: Container( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 200, height: 200, decoration: BoxDecoration( color: pannelColor, borderRadius: BorderRadius.all(Radius.circular(20.0))), child: Center( child: Text( title + pageCount.toString(), style: TextStyle( fontSize: titleTextStyle.fontSize, color: titleTextStyle.color, ), ), ), ), TextButton( onPressed: () { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (context) => FullScreenCustomPage( pannelColor: pannelColor, title: title, pageCount: pageCount + 1, ), ), ); }, child: Text('次画面へ')), ], ), ), ), ); } }
- 投稿日:2020-12-12T04:08:49+09:00
Android11でキーボードの表示/非表示を検知する
Android11で新たに導入されたAPIの中にキーボードの表示/非表示を検知するAPIが追加されたという情報を目にし、試してみました。
調べていく中でいろいろと仕組みをしっかり理解したほうがよさそうな雰囲気を感じたのですが今回はHowに焦点を絞ってまとめます。
実装
実装はいたって簡単です。
1.まず
WindowsInsetのコールバックを有効にするための設定を行いますFragmentoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requireActivity().window.setDecorFitsSystemWindows(false) }setDecorFitsSystemWindowsのAPIドキュメント
2.続いて
WindowInsetのアニメーションのコールバックをViewに設定しますどのViewに設定しても良いようなのでここではrootのviewに設定します
Fragmentoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setWindowInsetsAnimationCallback(object: WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { override fun onProgress( insets: WindowInsets, runningAnimations: MutableList<WindowInsetsAnimation> ): WindowInsets { return insets } }) }WindowInsetsAnimation.CallbackのAPIドキュメント
上記は必要最低限の実装になりますがキーボードの表示を検知するには
onProgressは向いていません。代わりに3.
onEndをオーバーライドしてキーボードの表示/非表示アニメーション終了を検知しますWindowInsetsAnimation.Callbackoverride fun onEnd(animation: WindowInsetsAnimation) { super.onEnd(animation) if (animation.typeMask == WindowInsets.Type.ime()) { val keyboardVisibility = view.rootWindowInsets.isVisible(WindowInsets.Type.ime()) val message = if (keyboardVisibility) "キーボード表示" else "キーボード非表示" Toast.makeText(context, message, LENGTH_SHORT).show() } }コード解説
上記実装の中の3つめの
onEndに関しては前提知識がないとなぜこんなコードになっているかわかりにくいと思うのでちょっとだけ解説します。
今回利用したWindowInsetsAnimation.Callbackはおそらくキーボードの動き以外も検知するものだと思われます(ここはしっかり調査したわけではないのですがWindowInsets.Typeがime以外にもいろいろあったためそう推測しました)
なのでanimation.typeMask==WindowInsets.Type.ime()を行いアニメーションの対象がキーボードかどうかを判定してからキーボードの表示状態をチェックしているという形になっています。所感
Android11以上でしか利用できないのでまだまだこの機能に頼ることはできませんが、今までの「Viewの変化を自分で検知してキーボード表示/非表示の判定をする」に比べると格段にわかりやすくなったと思います。
ただWindowInsetsについてはもっと理解をしなければとも感じたのでこれからも学習を続けたいと思います。参考・関連記事
- https://developer.android.com/about/versions/11/features?hl=ja
- https://developer.android.com/reference/android/view/View?hl=ja#setWindowInsetsAnimationCallback(android.view.WindowInsetsAnimation.Callback)
- https://developer.android.com/reference/android/view/Window#setDecorFitsSystemWindows(boolean)
- https://qiita.com/takahirom/items/c0864ad6ec2a6c013552
- https://qiita.com/HaSuzuki/items/f39e0857eaa7dcd88637
- https://github.com/android/user-interface-samples/tree/master/WindowInsetsAnimation
- 投稿日:2020-12-12T01:54:47+09:00
【Android】SpannableStringを使って文字列に画像を挿入する
はじめに
この記事はand factory Advent Calendar 2020 の12日目の記事です。
昨日は@k_shinnさんの 【Android】ちょっと便利なDrawableを書く でした。やりたいこと
- 画像を文字列として扱いたい
背景
Androidアプリを作っていく中で「テキストの横に画像を表示したいんだけど〜」
という旨のオーダーをもらうことがありまして
「そんなの簡単です!」と素直〜にImageViewの横にTextViewを置いて…と下記xmlで実装しました。hoge.xml<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:layout_marginBottom="20dp" android:background="@color/background"> <ImageView android:id="@+id/icon" android:layout_width="20dp" android:layout_height="20dp" android:src="@mipmap/ic_launcher" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/repository_name" android:layout_width="0dp" android:layout_height="wrap_content" android:ellipsize="end" android:textSize="20sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/icon" app:layout_constraintTop_toTopOf="parent" tools:text="テキストテキストテキスト" /> </androidx.constraintlayout.widget.ConstraintLayout>ですが長い文字列が来た際に困ったことが起きました。
そうです、文字の折り返し地点が不自然なものとなってしまうのです…!今回求められていた要件としては
文字列の2行目の位置がアイコンの真下に来るイメージのものが求められていました。★hogehoge
hogehogeそのため、SpannableStringでちょっとゴリった実装をしていくことにしました。
ザックリ言ってしまうと画像を挿入する分の文字スペースを作って
そのスペースにSpannableStringで画像をねじ込んでいくスタイルです。コード
IconSpannable.ktclass IconSpannable(private val targetTextView: TextView) { fun insertIconPrefix(@DrawableRes prefixDrawableRes: Int, sourceString: String): SpannableString { // 画像を挿入するため、一文字分の空白を作る val resultString = SpannableString(" $sourceString") return insertImage(prefixDrawableRes, resultString, 0, 1) } private fun insertImage( @DrawableRes drawableRes: Int, resultString: SpannableString, startSpan: Int, endSpan: Int ): SpannableString { val context = targetTextView.context // 対象のTextViewの高さを計測 val size = (targetTextView.paint.descent() - targetTextView.paint.ascent()).toInt() val drawable = context.resources.getDrawable(drawableRes, null) drawable?.let { it.setBounds(0, 0, size, size) val span = ImageSpan(it) resultString.setSpan(span, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) } return resultString } }使い方
binding.repositoryName.text = IconSpannable(binding.repositoryName).insertIconPrefix( R.mipmap.ic_launcher, "テキストテキストテキストテキストテキストテキスト" )結果
無事にTextViewの2行目が画像の下にでてきてくれました
終わりに
以上、SpannableStringを使って文字列に画像を挿入する方法でした!
単純そうな要件であっても意外とめんどくさいのがAndroid、というよりエンジニアの常ですよね。
どなたかの助けになれば幸いです。
- 投稿日:2020-12-12T00:15:34+09:00
AndroidTestをFastlaneで実行する
Androidアプリのインストゥルメント化単体テスト(ハードウェア デバイスまたはエミュレータで実行されるUIを使用したテスト)をCI環境に組み込むためにFastlaneから実行する方法のメモ。
背景
AndroidStudioからの実行はandroidTestディレクトリのコンテキストメニューから実行するという非常に直感的でわかりやすいが、完全に自動化しようとすると下記のステップを自動化する必要があり、自前では意外と簡単には行かなかった。
- エミュレータを起動
- 起動完了を待つ
- テストを実行
- テスト終了を待つ
- エミュレータを終了
前提
- Androidアプリ開発に必要なツールは一通りインストール済み
- Fastlaneを既にプロジェクトで使用している
TL;TR
AzimoLabs/fastlane-plugin-automated-test-emulator-run Fastlaneプラグインを使用することで、設定のみで実現できてしまう。
手順
プラグインのインストール
fastlane add_plugin automated_test_emulator_runプロジェクトのfastlaneディレクトリにAVD_setup.jsonを作成する
{ "avd_list": [ { "avd_name": "Nexus5_API_21_Play", "create_avd_package" : "system-images;android-21;google_apis;x86_64", "create_avd_device" : "Nexus 5", "create_avd_tag" : "google_apis", "create_avd_abi": "x86_64", "create_avd_hardware_config_filepath": "", "create_avd_additional_options": "", "launch_avd_snapshot_filepath": "", "launch_avd_launch_binary_name": "emulator", "launch_avd_port": "", "launch_avd_additional_options": "-gpu on" } ] }
create_avd_deviceではハードウェアプロファイルを指定する。AVD Manager GUI版では下記スクショの画面で選択するものにあたる。
avdmanager list deviceコマンドで表示されるリストのIDを指定する。% ~/Library/Android/sdk/tools/bin/avdmanager list device ... --------- id: 14 or "Nexus 9" Name: Nexus 9 OEM : Google --------- id: 15 or "Nexus One" Name: Nexus One OEM : Google --------- id: 16 or "Nexus S" Name: Nexus S OEM : Google --------- id: 17 or "pixel" Name: Pixel OEM : Google --------- id: 18 or "pixel_c" Name: Pixel C OEM : Google --------- id: 19 or "pixel_xl" Name: Pixel XL OEM : Googleで表示されるリストのIDを指定する。
リスト中の
id: 8 or "Nexus 5"
のところ。その他の詳細はこちら
https://github.com/AzimoLabs/fastlane-plugin-automated-test-emulator-run#json-configfastlane/Fastfileに下記の様にlaneを定義。
lane :run_android_test do # Delete existing emulator data to avoid failure of starting emulator sh("rm -rf ~/.android/avd/Nexus5_API_21_Play.avd") automated_test_emulator_run( AVD_setup_path: "fastlane/AVD_setup.json", gradle_task: "connectedDevelopmentDebugAndroidTest", gradle_flags: "-Pandroid.testInstrumentationRunnerArguments.class=com.smbc_card.vpoint.ui.email.EmailActivityTest", ) end
AVD_setup_pathに上記で作成したjsonファイルを指定する。
gradle_taskで指定するタスクはconnectedVariantNameAndroidTestとなる。つまり、フレーバーがDevelopmentでビルドモードがDebugならconnectedDevelopmentDebugAndroidTestとなる。
公式ドキュメントはこちら
gradle_flagsは必須ではないが、例では実行するテストクラスを指定している。
ここからの情報
sh("rm -rf ...")は前回作成されたavdのディレクトリが残っているとエラーになることがあったのだが、原因までは調べる時間なかったので、毎回削除することで回避している。下記コマンドで実行
bundle exec fastlane run_android_testHappy testing~!









