- 投稿日:2020-03-20T22:50:27+09:00
AndroidのDozeを回避してみようと思った話(回避できたとは言っていない)
はじめに
DozeはAndroid6から導入された素晴らしいバッテリー消費抑制機能です。
内容としては、バッテリー駆動かつ、静止状態かつ、画面OFFの状態が一定時間続くとで端末をスヤァーっとお眠りさせてバックグラウンド処理をさせなくするものです。
無法地帯と化していたバックグラウンドでのアプリの処理に制限をかけた事によりバッテリーの消費量は大幅に減ったと思います。
一般ユーザにはありがたい機能ですね。
しかし、企業向けアプリにとってはちょっと厄介な機能だと思っています。
顧客からバックグラウンドで位置情報やセンサーデータを取り続けたいんだーと言われたらやるしかありません。
そこで今回は、色々な方法でDozeを回避できるか試してしいこうと思います。
施策
駄目だった施策も交えつつ一つずつ試した結果を説明します。
施策1:フォアグラウンドサービスで処理を行う
結論から言いますが、駄目です。
制限されるのはバックグラウンド処理だけだからフォアグラウンドとして動作するフォアグラウンドサービスは大丈夫じゃないか?と思ったら大間違いです。きっちりDozeの対象です。
フォアグラウンドサービスにすることによってOSからプロセスをキルされることはほぼなくなりますが、それだけです。
https://qiita.com/naoi/items/03e76d10948fe0d45597
施策2:ホワイトリストに入れてDozeの条件から除外する
結論から言いますが、駄目です。
設定→電池→メニュー開く→電池の最適化で表示されるホワイトリストにアプリを追加するとDozeの影響をまったく受けないと思われるかもしれませんが、そうでもありません。
公式ドキュメントにも書いてある通り「このホワイトリストに含まれるアプリは、Doze モード中やアプリ スタンバイ中でもネットワークの使用が可能で、ネットワークの使用が可能で、部分的な wake locksを保持することができます。ただし、ホワイトリストに含まれるアプリにも、他のアプリと同様に他の制限は適用されます。」
AlarmManagerは抑制されるしGPSなども取得できなくなります。
施策3:TimerやAlarmManagerで定期的に画面をONにする
結論から言いますが、駄目になりました。
Android10からバックグラウンドプロセスからのActivity起動に制限が追加されたためActivityを起動して画面をONにすることができなくなりました。
https://developer.android.com/guide/components/activities/background-starts?hl=ja
Android9まではこの方法でDozeを回避可能でした。抜け道をAndroid10で潰したということですね。
ただ、Android10で追加されたこの制約は一般ユーザにはメリットです。
ある動画再生アプリが、アプリを利用していない時にも一定間隔で広告画面を全画面表示するというスパム行為をしていたのですが、そのような迷惑行為をできなくする効果があります。
施策4:Push受信時に通知を全画面Intentで表示する
結論から言いますが、Android10以上だと微妙ですがこの方法しかもはや方法が残っていません。
電話アプリが利用する全画面での通知表示機能を利用してActivityを起動し、そのActivityで画面をONにします。
Activityは画面をONにしたあとすぐに終了させればユーザの操作には問題ありません。
全画面での通知表示をする条件を画面OFF+バッテリー駆動状態にしておけば、無駄な起動もありません。
MyFirebaseMessagingService.kt/* push通知受信時処理は省略 */ val fullScreenIntent = Intent(context, TransparentActivity::class.java) val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT) val builder: NotificationCompat.Builder = createNotificationCompatBuilder(context) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(getString(R.string.app_name)) .setContentText("端末を強制的にWakeupさせました") .setWhen(System.currentTimeMillis()) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_CALL)// 着信画面じゃないけど着信カテゴリを設定・・・ .setFullScreenIntent(fullScreenPendingIntent, true)// 全画面インテントを設定TransparentActivity.kt// 画面ON処理(パーミッションの記載をAndroidManifestに忘れないように) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, "MyApp::MyWakelockTag").apply { acquire(1000) } }AndroidManifest.xml<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.WAKE_LOCK" />これをPush通知を受けるたびに実行するだけです。送信処理後すぐに端末が受け取るためにはPush通知は重要で送信する必要があります。
ここまですればこのアプリだけDozeを回避することが可能です。というかPush通知を利用している時点で定期的な処理が数十秒可能になります。
本来は画面を強制的にONにすることによって、他のアプリもすべてDozeの影響を受けなくしたかったのですが、Android10, Pixel3aで試したところDoze状態になったログが残りました。
しかし、定期的なPush通知の受信も画面ONもできていたためPushを受信しているアプリは問題なくバックグラウンド処理ができているようです。
なんとも微妙な結果ですが、仕方ありません。Android10からアプリごとにDozeの条件が管理されるようになったのかもしれません。
自分で試してみたい方へ
Dozeは、 端末メーカーによって微妙に挙動が変わるためこの結果が確実ではない可能性があります。
信じられない場合は、自身でサンプルアプリを作って試すことをおすすめします。
また、施策4だけは、下記アプリを入れて確認ができます。
Doze Buster
https://play.google.com/store/apps/details?id=jp.nittan.dozebuster&hl=jpDoze状態になっているのかどうかのログを残すだけにも利用できるのでどうぞお使いください。
- 投稿日:2020-03-20T18:32:50+09:00
ActivityResultContractのコードを少し読んでみる
Activity 1.2.0-alpha02 と Fragment 1.3.0-alpha02で追加されたAPIのようで、型安全にstartActivityForResultを扱えるようです。またテストもできるようです。
コード例
AppCompatActivityやFragmentでprepareCall()でlauncherを用意して、launchを呼ぶとstartActivityForResultを行うことができるようです。MainActivity.ktval intent = Intent(this, SecondActivity::class.java) val launcher: ActivityResultLauncher<Intent> = prepareCall( ActivityResultContracts.StartActivityForResult() ) { activityResult: ActivityResult -> Log.d("MainActivity", activityResult.toString()) // D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }} } fab.setOnClickListener { view -> launcher.launch(intent) }SecondActivity.ktclass SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data")) finish() } }コードを剥がしていく
ちょっと魔法みたいなので、中身を覗いて、今のコードに適応させてみましょう。
ActivityResultContractを直接使ってみる
ActivityResultContracts.StartActivityForResultクラスは以下のように
ActivityResultContractを継承しています。ActivityResultContracts.javapublic class ActivityResultContracts { private ActivityResultContracts() {} ... public static class StartActivityForResult extends ActivityResultContract<Intent, ActivityResult> { @NonNull @Override public Intent createIntent(@NonNull Intent input) { return input; } @NonNull @Override public ActivityResult parseResult(int resultCode, @Nullable Intent intent) { return new ActivityResult(resultCode, intent); } }そのため
ActivityResultContractを使って書き直して以下と同じです。
ここでは型引数としてIntentとActivityResultクラスをとっています。このように自分でstartActivityのときに使いたい引数の型とActivityResultで受け取る型も型引数で指定して書いていくことができます。MainActivity.ktval launcher: ActivityResultLauncher<Intent> = prepareCall( // ** ↓ ** object : ActivityResultContract<Intent, ActivityResult>() { override fun createIntent(input: Intent): Intent { return input } override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult { return ActivityResult(resultCode, intent) } } // ** ↑ ** ) { activityResult: ActivityResult -> Log.d("MainActivity", activityResult.toString()) // D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }} }ActivityResultRegistryを直接使ってみる
またComponentActivity.prepareCallは以下のように、mActivityResultRegistryにregisterActivityResultCallback()しています。
ComponentActivity.java@NonNull @Override public <I, O> ActivityResultLauncher<I> prepareCall( @NonNull ActivityResultContract<I, O> contract, @NonNull ActivityResultCallback<O> callback) { return prepareCall(contract, mActivityResultRegistry, callback); } @NonNull @Override public <I, O> ActivityResultLauncher<I> prepareCall( @NonNull final ActivityResultContract<I, O> contract, @NonNull final ActivityResultRegistry registry, @NonNull final ActivityResultCallback<O> callback) { return registry.registerActivityResultCallback( "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback); } @NonNull public ActivityResultRegistry getActivityResultRegistry() { return mActivityResultRegistry; }そのため
ComponentActivity.getActivityResultRegistry()をしてActivityResultRegistry.registerActivityResultCallback()を利用すると以下のように書くことができます。MainActivity.kt// ** ↓ ** val launcher: ActivityResultLauncher<Intent> = activityResultRegistry .registerActivityResultCallback( "activity_rq#0", // ←この数字は本来は呼び出すたびにincrementが必要 // ** ↑ ** object : ActivityResultContract<Intent, ActivityResult>() { override fun createIntent(input: Intent): Intent { return input } override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult { return ActivityResult(resultCode, intent) } } ) { activityResult: ActivityResult -> Log.d("MainActivity", activityResult.toString()) // D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }} }
registerActivityResultCallback(key, ActivityResultCallback, ActivityResultContract)を使うとActivityResultRegistoryが持っているHashMapにputされますComponentActivity周りのかんたんな構造
単純にActivityがActivityResultRegistoryを持っていて、ActivityResultRegistoryがActivityResultContractとActivityResultCallbackを持っているようです。
このHashMapのキーはregisterActivityResultCallbackに渡される"activity_rq#" + mNextLocalRequestCode.getAndIncrement()などになっています。
今まであったrequest codeはどうなったか?
今まではstartActivityForResultを呼び出すときに呼び出しと返却を識別するために使うrequest codeというのが必要でしたが、それが利用するときに必要なくなりました。こちらもAutoIncrementして管理しています。また、プロセスが死んでも再度使えるように
onSaveInstanceStateでrequest codeとActivityResultRegistoryのkeyのペアを管理して、保存してうまくやってくれるようですActivityResultRegistry.javaprivate int registerKey(String key) { Integer existing = mKeyToRc.get(key); if (existing != null) { return existing; } int rc = mNextRc.getAndIncrement(); bindRcKey(rc, key); return rc; }Fragmentでメモリリークしないのか?
Fragment.prepareCall()の実装を見るとFragmentでON_CREATEになったときにgetActivity().getActivityResultRegistry().registerActivityResultCallbackします。Activityのスコープの変数にFragmentで作られたコールバックを突っ込むため、
これだけ見るとリークしてそうに見えます。Fragment.javapublic <I, O> ActivityResultLauncher<I> prepareCall( @NonNull final ActivityResultContract<I, O> contract, @NonNull final ActivityResultCallback<O> callback) { ... getLifecycle().addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { if (Lifecycle.Event.ON_CREATE.equals(event)) { ref.set(getActivity() .getActivityResultRegistry() // **ここでregisterActivityResultCallback** .registerActivityResultCallback( key, Fragment.this, contract, callback)); } } }); return new ActivityResultLauncher<I>() { @Override public void launch(I input) { ... } }; }しかし、以下でLifecycleOwnerとして渡しています。
Fragment.javaregisterActivityResultCallback( key, Fragment.this // **←ここでLifecycleOwnerとして渡す**また、以下のようにON_DESTROYでunregisterActivityResultCallbackするため問題なさそうに見えます。
ActivityResultRegistry.javalifecycle.addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { if (Lifecycle.Event.ON_DESTROY.equals(event)) { unregisterActivityResultCallback(key); } } });まとめ
- ActivityResultContractに型引数を渡すことで、渡す方と受け取る型を指定していけそう。
- ComponentActivity.ActivityResultRegistryでコールバックなどは管理されている。
- この部分に関してFragmentのリークは気にしなくても良さそう。
参考
https://android-review.googlesource.com/c/platform/frameworks/support/+/1233518
https://developer.android.com/training/basics/intents/result
https://medium.com/@star_zero/activityresultcontract%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Factivity%E9%96%93%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97-17052bfafde0
- 投稿日:2020-03-20T16:06:02+09:00
Android (Kotlin) でブロードキャスト送受信の仕方を理解する
きっかけ
最近、Android 開発を仕事で行っていますが、複数のアプリ間連携の方法を今まで知りませんでした。
最近知ったのですが、IntentとBroadcastReceiverを使用することで簡単にアプリ間連携が可能でした。しかし、まったくの初心者であった自分にとっては???でした。。
そこで今回は 2 つのアプリを連携させるIntentの使い方について学ぶことができたので、共有します。環境
- Android Studio 3.6.1
- Android 7.0 以降
概要
1 つの Android 端末に 2 つのアプリケーションをインストールし、その 2 つのアプリ間の連携方法について記述します。
今回作成するアプリは以下になります。IntentBroadcast アプリ
Intentを Android 端末アプリケーション内の全てのアプリにブロードキャストするアプリケーションIntentReceiver アプリ
- ブロードキャストされた
Intentを受信するアプリケーション1. IntentBroadcast アプリについて
Activity 画面にボタンを配置し、そのボタンをタップすることで
Intentを送信します。
ブロードキャストの方法は、以下のようにIntentインスタンスを作成し、sendBroadcast(intent)を実行することで可能です。MainActivity.ktimport android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ボタン1配置 button1.setOnClickListener { // インテントをブロードキャスト val intent = Intent("com.example.broadcast.MY_NOTIFICATION1") intent.putExtra("data", "Notice me senpai!") sendBroadcast(intent) } // ボタン2配置 button2.setOnClickListener { // インテントをブロードキャスト val intent = Intent("com.example.broadcast.MY_NOTIFICATION2") intent.putExtra("data", "Hello, world!") sendBroadcast(intent) } } }ここで、
Intentのインスタンス作成時に以下のように Action 文字列をセットしています。
Intentを受信するレシーバはこのcom.example.broadcast.MY_NOTIFICATION1の部分を確認し、受け取るべき情報なのかを判断します。そのため一意であればどんな文字列でも構いませんが、作法として「パッケージ名 + 任意の文字列」とするのがベターです。
(Android 端末内のアプリケーションのパッケージ名は基本的に一意であるためです。)また、
IntentはputExtra()を利用することで情報を追加登録することができます。
今回はdataという名前でそれぞれ文字列をセットしました。
ちなみに以下のactivity_main.xml、strings.xmlは画面にボタンを設置するために記述しているだけで、ブロードキャスト処理には関係ありません。
上記のソースを動かす際のサンプル用として参考してください。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" android:padding="50dp" tools:context=".MainActivity"> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button1" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button2" /> </LinearLayout>strings.xml<resources> <string name="app_name">IntentBroadcast</string> <string name="button1">Button1</string> <string name="button2">Button2</string> </resources>2. IntentReceiver アプリについて
こちらは Android 端末内にブロードキャストされている
Intentを受信します。
受信の方法はまず、BroadcastReceiverクラスを継承した、ブロードキャストを実際に受信するクラス(MyBroadcastReceiverクラス)を作成します。そしてメイン画面に、先ほど作成した
MyBroadcastReceiverクラスを利用するレシーバを準備することでIntent受信が可能となります。MyBroadcastReceiver.ktimport android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.widget.Toast class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { // インテントに登録されている、名前"data"に対応する文字列をトーストで表示する Toast.makeText(context, intent?.getStringExtra("data"), Toast.LENGTH_SHORT).show() } }MainActivity.ktimport android.content.IntentFilter import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // BroadcastReceiverを継承したクラス(MyBroadcastReceiver)を指定する // IntentFilterは何を呼び出すかを振り分けるもの // 2020/03/20: links_2_3_4 さん指摘反映 // 画面回転時にメモリリークに関する警告表示を阻止 this.applicationContext.registerReceiver( MyBroadcastReceiver(), IntentFilter("com.example.broadcast.MY_NOTIFICATION1") ) this.applicationContext.registerReceiver( MyBroadcastReceiver(), IntentFilter("com.example.broadcast.MY_NOTIFICATION2") ) } }ここで
MainActivity.ktでは、Intentの Action 文字列によってどのブロードキャストレシーバクラスを利用するかを設定しています。
(今回は同じブロードキャストレシーバクラスMyBroadcastReceiver()を設定しています。)そして
Intentの Action 文字列(com.example.broadcast.MY_NOTIFICATION1)が、IntentFilter()内でで記述した文字列と同じであれば、設定したブロードキャストクラス内の処理が実行されます。ブロードキャストレシーバクラス
MyBroadcastReceiver.ktでは、Intentに登録された名前dataの文字列をトーストで表示するようにしました。挙動確認
- IntentBroadcast アプリと IntentReceiver アプリを Android 端末にインストールします。
- IntentReceiver アプリを起動し、ホームボタンを選択するなどしてバックグラウンドに移動させます。
- IntentBroadcast アプリを起動し、画面上に設置したボタンをタップします。
うまくブロードキャスト送信・受信ができれば
Intentに登録したdataの文字列がトースト表示されます。
これで、IntentBroadcast アプリで操作した情報が IntentReceiver アプリに受け渡され、2 つのアプリ間連携ができました。
最後に
今回は、1 つのアプリケーションで
Intentをブロードキャストを行い、もう 1 つのアプリケーションでIntentを受け取ってトースト表示する仕組みを紹介しました。
本記事ではとても単純なケースを紹介しましたが、このIntentを使えば、複数の Android アプリケーションでの連携が可能になります。
今後余裕があれば、このIntentのやり取りを利用した他のサンプルも実装・紹介できたらなと思います。最後まで閲覧いただき、ありがとうございました。
- 投稿日:2020-03-20T15:59:11+09:00
GIFアニメーションをImageViewで表示する
はじめに
需要があるかはわからないですが、Gifアニメをこの度自プロジェクトでAndroidアプリ上で動かしたいという要望があったので調べました。
調べてみた
まずは、「Gif animation android imageview」などでググってみました。
すると・・・glideというライブラリを使うといいよ!
という記事がたくさん出てきました。https://qiita.com/maehara08/items/eef63804c52b2e0bb404
それ以外だと、Movieクラスを使ってCanvasに一枚ずつ書き出して・・・みたいなゴリ押しパターンがありました。
https://tomorrowkey-2.hatenadiary.org/entry/20100419/1271678022弊社では、ライブラリを使うにはちょっと時間がかかるのと、外部の信用できる会社のものでないと結構たいへんなので、glideライブラリを使うというのは避けたかったので、ゴリ押しパターンを実装してみようということになりました。
準備
Gifアニメのダウンロード
とりあえず、フリー素材のGifアニメをダウンロード。
下記のサイトからもらいました。ありがとう。
https://sozai-good.com/illust/gifanimation/assetsフォルダの作成
とりあえず表示するだけなので、assetsにダウンロードしたGifアニメを置くことに。
projectツリーのappあたりで右クリックして
[New]->[Folder]->[Assets Folder]をすると簡単につくれます。
assetsフォルダがapp->src->mainの下にできていたら、ダウンロードしたGifアニメを入れます。
Gifアニメのファイル名は適当に。
レイアウトファイル
今回は、Activityの画面にImageViewを配置するだけにします。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout 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"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/image_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>Gifアニメを表示させる
Gifアニメを表示する場合、
Movieがversion28から非推奨になっているため、
コードをOSごとに分ける必要があります。Android P以上の場合
リファレンス通りに、
MovieはdeprecatedなのでAnimatedImageDrawable
Movieのリファレンス
AnimatedImageDrawableのリファレンスAnimatedImageDrawableを、assetsからGifアニメを取得して表示します。
@RequiresApi(Build.VERSION_CODES.P) private fun getGifAnimationDrawable(): AnimatedImageDrawable { val source = ImageDecoder.createSource(assets, "gif_anim_toy_poodle.gif") return ImageDecoder.decodeDrawable(source) as? AnimatedImageDrawable ?: throw ClassCastException() }手順は簡単です。
1.ImageDecoder.createSourceで、第一引数にassetManager、第二引数にassetsに入っているファイル名を指定してください。
ちなみに、このImageDecoder.createSourceは他にもFileを指定したり、res & resIDを指定したり、ContentResolver & URIなど、複数のファイル取得に対応していそうです。
2. 1で取得したImageDecoder.SourceインスタンスをImageDecoder.decodeDrawable()に渡してDrawableを取得します。
3.AnimatedImageDrawableにキャストします。取得ができたら、ImageViewに設定します。
// AnimatedImageDrawableを取得する val drawable = getGifAnimationDrawable() // アニメーションをセットする binding.imageView.setImageDrawable(drawable) // アニメーションを開始する drawable.start()Android P未満の場合
Android P未満の場合だと、
ImageDecoderとAnimatedImageDrawableが利用できません。
なので、従来の通りMovieクラスを使った方法を一応書いておきます。参考にしたサイトはこちらで、基本的にこちらの写経です。
https://end0tknr.hateblo.jp/entry/20120714/1342247240カスタムのDrawableを用意する
MovieクラスでのGifアニメーション起動の方法は、onDrawでMovieを動かす方法になるので、
カスタムクラスを作る必要があります。
参考にしたサイトではViewを継承していましたが、今回はImageViewに設定したかったので、Drawableを継承しました。CustomAnimatedDrawable.ktclass CustomAnimatedDrawable( private val inputStream: InputStream ): Drawable() { private val _movie by lazy { Movie.decodeStream(inputStream) } private var _movieStart = 0 private var _loop = true private var _stop = false private var relativeMillisecond = 0 override fun draw(canvas: Canvas) { canvas.apply { drawColor(Color.TRANSPARENT) scale(width / _movie.width().toFloat(), height / _movie.height().toFloat() ) } val now = SystemClock.uptimeMillis() if (_movieStart == 0) { _movieStart = now.toInt() } relativeMillisecond = when { _stop -> { _movieStart = 0 relativeMillisecond } _loop -> ((now - _movieStart) % _movie.duration()).toInt() else -> (now - _movieStart).toInt() } _movie.apply { setTime(relativeMillisecond) draw(canvas, 0f, 0f) } invalidateSelf() } override fun setAlpha(alpha: Int) { } override fun getOpacity(): Int = PixelFormat.UNKNOWN override fun setColorFilter(colorFilter: ColorFilter?) {} fun stop() { _stop = true } fun start() { _stop = false _movieStart = 0 } fun isRunning() = !_stop }assetsからファイルをopenする
あとはassetsからファイルを取得して、カスタムクラスにinputStreamを渡してあげればOKです。
private fun getGifAnimationDrawableLessThanP(): CustomAnimatedDrawable { val inputStream = assets.open(ANIMATION_GIF_FILE_NAME) return CustomAnimatedDrawable(inputStream) }これでOKです。
キャプチャ
recordで動画を撮ったんですが、いつからかわからないですが、.webm方式での保存になってしまい、かつadb shell screen record..みたいなのもうまく動かないので、泣く泣くキャプチャを取るしかできませんでした。
この子がペケペケ動きます。コード
サンプルのコードはここにおいておきました。
なんかpushの仕方間違えちゃったので一階層無駄があります。
https://github.com/keikyukyun/gif_animation_sample最後に
久しぶりにgitにもアップしたけど、webviewなら一撃という噂も。
- 投稿日:2020-03-20T15:20:20+09:00
FlutterでURLSchemeを使って他のアプリ(例: Twitter)を開く(開けない場合の回避策も)
Flutterで個人開発をしている村松龍之介と申します。
(仕事ではiOSアプリのネイティブアプリ開発を行っています)今回は、アプリに自分のTwitterアカウントのリンクを載せようと思いました。
最初は普通にURLを貼って、ブラウザで開くようにしていたのですが、アプリ入ってるならアプリが開いた方が良いよね…と思い実装した備忘録です。URLを開くためにurl_launcherパッケージを使う
まず、URLを開くために必要な
url_launcherパッケージを導入済みでなければ導入します。
url_launcher | Flutter Package簡単に書きますと、
pubspec.yamlファイルのdependenciesに1行追記します。
VS Codeですと、⌘ + S で保存すればflutter pub getが自動で走りますのでインストールできます。
Android Studioでもおそらく同じ?dependencies: url_launcher: ^5.4.2 # <-導入時点で最新のバージョンで良いかと思います他のアプリを開きたいファイル(クラス)に実装します
ここではTwitterのプロフィールページを例に用います。
url_launcherパッケージをインポート
import 'package:url_launcher/url_launcher.dart';URLを開く
launch(url)関数で引数に設定したURLを開けます。
非同期な関数なのでawaitを使っています。final url = 'twitter://user?screen_name=riscait' // <-Twitterアプリのユーザープロフ画面を開くURLScheme await launch(url);開くことのできないURLが入ってくる可能性がある場合は
canLaunch(url)で調べることができます。if (await canLaunch(url)) { await launch(url); } else { // 任意のエラー処理 }URLを開けなかったときのためのセカンドURLを用意
final url = 'twitter://user?screen_name=riscait' // <-Twitterアプリのユーザープロフ画面を開くURLScheme final secondUrl = 'https://twitter.com/riscait' // <-Twitterアプリのユーザープロフ画面を開くURL if (await canLaunch(url)) { await launch(url); } else if (secondUrl != null && await canLaunch(secondUrl)) { // 最初のURLが開けなかった場合かつセカンドURLが有って開けた場合 await launch(secondUrl); } else { // 任意のエラー処理 }メソッド化
/// 第2引数のURLを開く。開けないURLだった場合は第2引数のURLを開く Future _launchURL(String url, {String secondUrl}) async { if (await canLaunch(url)) { await launch(url); } else if (secondUrl != null && await canLaunch(secondUrl)) { await launch(secondUrl); } else { // 任意のエラー処理 } }ボタンで使用する一例
RaisedButton( child: const Text('Twitterを開く'), onPressed: () => _launchURL( 'twitter://user?screen_name=riscait', secondUrl: 'https://twitter.com/riscait', ), ),iOSのためにinfo.plistを編集する
これでAndroidではTwitterアプリがインストールされていれば開かれることを確認しました!
しかし、iOSに対応する場合にはもう一手間必要です。Android StudioやVS Codeで編集する場合
info.plistを開きます。
下記のように1行追加します。<key>LSApplicationQueriesSchemes</key> <array> <string>itms</string> <string>twitter</string> <!-- この1行を追加 --> </array>僕の環境下では
LSApplicationQueriesSchemesが既に存在しましたが、なかった場合は、5行全部追記しましょう!<plist version="1.0"> <dict> <!-- この間に追記すればOK --> </dict>Xcodeで(Property Listとして)開いて編集する場合
LSApplicationQueriesSchemesの一要素として以上、これでiOSでもアプリがインストールされていればアプリが開くようになります。
TwitterのURL Schemeについてはこちらのサイトに詳しく載っておりました?
【2019】Twitter公式Appのスキーム一覧 │ えぐぷと!蛇足
ご覧いただきありがとうございました!
蛇足ですが、Flutterアプリをリリースできたので良かったらインストールしてみてもらえると嬉しいです?♂️
iOS: 「レストル-有給休暇管理」をApp Storeで
Android: レストル-有給休暇管理 - Google Play のアプリ
- 投稿日:2020-03-20T04:01:40+09:00
onConfigurationChangedで回転時のレイアウトを変更
やりたかったこと
ImageViewの画像を画面いっぱい(アスペクト比はそのまま)に表示してかつ、ボタンを真下に置きたかったのですが、、、
横回転するとボタンが隠れてしまうし、横画面に合わせてレイアウト変えると縦回転したときにボタンの位置がずれてしまってました。
レイアウト用のxmlは1つだけにしたかったので、ソース内で動的にImageViewのサイズを変えることにしました。やったこと
レイアウト
- レイアウトはConstraintLayoutを使う
- ImageViewの設定:
- サイズ(縦向き時):
- height: wrap_content
- width: 0dp(match constraint)
- サイズ(横向き時):
- height: 0dp(match constraint)
- width: wrap_content
- 描画の縦横比を維持するために
android:adjustViewBounds = trueを指定- ボタンをくっつけるために
app:layout_constraintVertical_chainStyle="packed"を指定Activity
- マニフェストで、Activityに
android:configChanges="orientation|screenSize"を追加。縦横の回転を検出できるようにする。- 縦横の向きが変わると
onConfigurationChangedがコールされる。その中で、ImageViewのレイアウトを更新。ソース
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"> <ImageView android:id="@+id/imageView" android:layout_width="0dp" android:layout_height="wrap_content" android:adjustViewBounds="true" android:padding="8dp" app:layout_constraintBottom_toTopOf="@+id/button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_chainStyle="packed" app:srcCompat="@drawable/ic_launcher_background" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" android:text="Button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imageView" /> </androidx.constraintlayout.widget.ConstraintLayout>MainActivity.javapublic class MainActivity extends AppCompatActivity { ImageView imageView; Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //UI取得 imageView = findViewById(R.id.imageView); button = findViewById(R.id.button); //一応、起動時の向き取得 int orientation = getResources().getConfiguration().orientation; UpdateLayout(orientation); //レイアウト更新 } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); int orientation = newConfig.orientation; //向き取得 UpdateLayout(orientation); //レイアウト更新 } /** * レイアウト更新 * @param orientation 向き */ protected void UpdateLayout(int orientation){ //ImageViewのLayoutParam取得 LayoutParams param = imageView.getLayoutParams(); if (orientation == Configuration.ORIENTATION_PORTRAIT) { // 縦向きの場合 Log.d("UpdateLayout", "縦向き"); param.height = LayoutParams.WRAP_CONTENT; param.width = 0; } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { // 横向きの場合 Log.d("UpdateLayout", "横向き"); param.height = 0; param.width = LayoutParams.WRAP_CONTENT; } imageView.setLayoutParams(param); } }参考記事
Androidで横向きか縦向きかを判定する
Androidで画像の縦横比を保ったまま画面幅いっぱいに表示する
https://techblog.zozo.com/entry/constraint_layout
- 投稿日:2020-03-20T01:24:07+09:00
Android単体でADBを使う。(迫真AARCH64部、Linuxの裏技)
注意
- 完全に単体でやるには開発者向けオプションにネットワーク経由でデバッグがないと無理。
- ネットワーク経由でデバッグがないと再起動する度tcpipを、使わざるを得なくなる…GoogleはAndroid12で改善してくれるのか!?
本編:
プロローグ
(Y)ぬわああああああん疲れたもおおおおおおおん
(M)チカレタ…
(Y)じゃけん風呂入って寝ましょーねー
(Y)おっそうだな(自問自答)
ここからベッドシーンへ…
(Y)ファッ!このアプリADB権限いるのか!?
(Y)やめたくなりますよー……
(M)あっそうだ!Androidにadbぶち込んでやれば解決だゾ〜
(Y)それができれば苦労して ないです(断言) aarch64バイナリは提供されてない、はっきりわかんだね!
(K)や!、せんぱい! aarch64バイナリがありましたよ!
(Y)ファッ!(デデドン!) クゥーン?
(M)よし!じゃあ打ち込んでやるぜ!ゾキューピー3分クッキングの曲の始まり
本編
今回は中に入れます!(Archlinuxの!)
なのでじゃけん環境構築していきましょーねー(ベッドで書いてるからここもベッドシーン)
Requirements
ぶち込んで!(語彙力欠損)
そのためのRDME??Termux
TermuxArch
https://github.com/TermuxArch/TermuxArch
ADB
やべぇよやべぇよ…こんなとこにaarch64のadbバイナリがぁあああ……あ、
https://github.com/qhuyduong/arm_adb/releases
こいつをwget (俺が好きなだけ。好きなラーメンの味と一緒)
あとは簡単麺(なんちって。(激寒))
- ネットワーク経由のデバッグをオンにする'
- この項目がない場合は
tcpipを用いる- ??⇩見て⇩???
termuxarchchmod a+x ./adb ./adb connect ipaddrや、やりますねぇ!






