Be simple

”当たり前”が誰かのためになる

Androidアプリ開発者に対して、アプリからアプローチを取る手段を考えてみた

はじめに

こんにちは

今日は、以前から開発者に対してアプリからアプローチを取るにはどうしたら良いか、考えてみたので、そのアプローチの仕方の一例の紹介です。

といっても、今回はコラムとして捉えていただきたいです。

 

 

では、さて本題です。

Androidエンジニア採用が困難な昨今、Androidアプリエンジニアが欲しいといった声やTwitterでの嘆きがよくみられます。

 

確かに、Androidアプリエンジニアがなかなか見つからない、といった状況があるのも事実です。

 

そこで考えられる手段として自分は2つあると思っています。

  1. Androidの開発に関するやり方をわかりやすく伝える
  2. すでに開発しているAndroidエンジニアにアプローチを行う

今回は、2について焦点を当てて、1つだけ考えていたことを紹介したいと思います。

 

考えていたこととは、ズバリ…

 

Androidエンジニアなら、(人によるけど)実機でビルドを行なっているので、開発者モードにしていてかつUSBケーブルを接続しているときに何かしらメッセージなどを表示すれば良いのではなかろうか…!!

(エミュレーターで開発している場合を除く…)

 

こういったモチベーションで次のようなサンプルを作成してみました。

 

 今回は、ただ画像をアニメーションさせているだけですが、開発者モードかつUSBケーブル接続時に、例えばDialogを出して「あなたも開発者になりませんか?」とか、バナーを変えたりとかできたら、アプリからアプローチできるよなーって思った次第です。

 

アプローチ自体はアイディア次第だと思うので、今回は「開発者モードかつUSBケーブル接続時」という条件のもと、画像をアニメーションさせるまでの実装方法についてご紹介させていただきます。

 

今回紹介するサンプルはこちら

github.com

 

UsbManagerでいけんじゃね?と思ってサンプルを作ったのですが、UsbManagerは使ってません。リポジトリ名は気にしないでください。

 

それでは実際に作っていきましょうー

 

 

 「開発者モードかつUSBケーブル接続時」という条件のもと、画像をアニメーションさせるまでの実装方法について

条件の部分だけ知りたい方は「ここから本題」からみてください

まずは準備

 個人的にDataBindingを使いたかったので、app配下のbuild.gradleに次のコードを記載してください。

dataBinding {
enabled = true
}

findViewByIdでviewをさわれば良いので、なくてもできます。

 

アニメーションさせるImageViewを持つレイアウトを準備

activity_main.xmlを自分は次のように用意しました。

単純に真ん中にImageViewを用意して、drawableに入れたいらすとやの画像を表示するだけです。

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/hero"
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>

レイアウトの準備が終わったら、本題となる条件の部分についてやっていきましょう。 

 

ここから本題

本題となる「開発者モードかつUSBケーブル接続時」という条件についてです。

紹介するサンプルのコード(MainActivity)の全容が次のとおりです。

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
setUp()
}

private fun setUp() {
val disableDevelopmentMode = Settings.Secure.getInt(this.contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) == 0
val usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (disableDevelopmentMode) {
return
}

val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = context.registerReceiver(null, filter)
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
if (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) {
val animation = ValueAnimator.ofFloat(1500f, 0f)
animation.interpolator = AccelerateInterpolator()
animation.duration = 500L
animation.addUpdateListener {
binding.image.translationY = animation.animatedValue as Float
}
animation.start()
} else {
val animation = ValueAnimator.ofFloat(0f, 1500f)
animation.interpolator = AccelerateInterpolator()
animation.duration = 500L
animation.addUpdateListener {
binding.image.translationY = animation.animatedValue as Float
}
animation.start()
}
}
}
val filter = IntentFilter()
filter.addAction(Intent.ACTION_POWER_CONNECTED)
filter.addAction(Intent.ACTION_POWER_DISCONNECTED)
registerReceiver(usbReceiver, filter)
}
}

 

ポイントとなる条件は2つです

  1. 開発者向けオプションをONにしているかどうか
  2. USB接続時かどうか

 

まずは、開発者向けオプションをONにしているかどうかについてです。

次の記事を参考にさせていただきました。

qiita.com

 

APIレベル17から利用できるSettings.Global.DEVELOPMENT_SETTINGS_ENABLEDを使います。

今回は開発者向けオプションをOFFにしている場合のフラグをdisableDevelopmentModeとしています。コードでいうと、次の部分です。

val disableDevelopmentMode = Settings.Secure.getInt(this.contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) == 0

Settings.Secure.getInt(this.contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0)で開発者向けオプションをONにしているかどうか取得でき、0であればfalse、1であればtrueになります。

これで、開発者向けオプションをONにしているかどうかの条件をクリアできました。

 

次に、USB接続時かどうかについてです。

今回は、USB接続時かどうかについて、給電中かどうかで判断しています。

実際の実装は次のとおりです。

val usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (disableDevelopmentMode) {
// 開発者向けオプションをOFFにしているときは早期リターン
return
}

val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = context.registerReceiver(null, filter)
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)

if (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) {
// USBケーブルを接続した時の処理を書く
} else {
// USBケーブルを外した時の処理を書く

}
}
}
val filter = IntentFilter()
filter.addAction(Intent.
ACTION_POWER_CONNECTED)
filter.addAction(Intent.
ACTION_POWER_DISCONNECTED)
registerReceiver(usbReceiver
, filter)

 

まず、BroadcastReceiverを持つ変数usbReceiverを宣言します

val usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {

 

onReceiveのなかで、次のように書くことで、Batteryの状態を取得することができます。

override fun onReceive(context: Context, intent: Intent) {
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = context.registerReceiver(null, filter)
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
}

これで、Androidのシステムから送られてくるBatteryの変化についてのBroadcastを受信し、その状態をstatusとして持つことができるようになりました。

 

あとは、次のようにして比較を行います。

if (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) {
// USBケーブルを接続した時の処理を書く
} else {
// USBケーブルを外した時の処理を書く
}

BatteryManager.BATTERY_STATUS_CHARGINGは充電中を表し、BatteryManager.BATTERY_STATUS_FULLは充電完了を表します。

 

statusはBatteryの変化についての状態を持っているので、上記2つとの比較を行い、USBケーブルを接続しているかどうかの条件として扱います。

 

最後に、ACTION_POWER_CONNECTEDとACTION_POWER_DISCONNECTEDだけを受信するようにfilterを作成して、receiverを登録します。

val filter = IntentFilter()
filter.addAction(Intent.ACTION_POWER_CONNECTED)
filter.addAction(Intent.ACTION_POWER_DISCONNECTED)
registerReceiver(usbReceiver, filter)

これで実装完了です。 

 

他のやり方も調べてみたものの、Android6系からdefaultで給電になるようですし、Usbがattachしたかどうかでやってみたもののうまく実現しなかったためこちらの手段を取りました。もし、うまいやり方が他にあれば教えてください、お願いしますー

 

とりあえずこれで、USB接続時かどうかの条件をクリアすることができました。

 

最後は、アニメーションについてです。

ValueAnimatorを用いて実装できます。

上から下に動くアニメーションは次のように書きます。

val animation = ValueAnimator.ofFloat(1500f, 0f)
animation.interpolator = AccelerateInterpolator()
animation.duration = 500L
animation.addUpdateListener {
binding.image.translationY = animation.animatedValue as Float
}
animation.start()

ValueAnimator.ofFloat("始点(Float)", "終点(Float)")としてanimationを作成します。

interpolatorで速さの変化についてお好きなものを設定し、アニメーションの期間はdurationをLongで指定します。

それが終わったら、addUpdateListenerでY軸方向に先ほど作成したanimationの値を入れて、startするだけで実現できます。

下から上に動く場合には、始点と終点を逆にするだけで良いです。

 

他にも、View.OnAttachStateChangeListenerでやる方法などありますが、USBケーブル接続時に構成が変わってしまいうまくアニメーションしない時があるので、ValueAnimatorで作成しました。

 

 「開発者モードかつUSBケーブル接続時」という条件のもと、画像をアニメーションさせるまでの実装方法について、は以上です。

 

これで、開発者オプションのON/OFFで、アプリを立ち上げなおすと「はじめに」で紹介したTwitterの動画のような動きを実現することができます。

一応、その際のデモが次のとおりです。

f:id:rozkey59:20190424153649g:plain

sample

 

おわりに

今回の例はあくまでも1例です。他にももっと良いやり方やアイディアがあると思います。

ただ、少しでもAndroidエンジニアが増えるといいですよね。

 

はじめに、であげた2のアプローチするまでの紹介になりましたが、1のAndroidの開発に関しても、他のエンジニアの方もやっているように技術発信をどんどんして、みんながAndroid開発できるように、わかりやすく伝えられるように今後もやっていきたいと思います。

 

少しでも参考になったら嬉しいです。不明点などあれば、コメントでお願いいたします。

それでは、またの機会に。