- 投稿日:2020-08-04T18:59:17+09:00
Androidのキーボード表示非表示を検知する最も簡単な方法(だと思う)
Androidではキーボードの表示/非表示を検知する方法が標準では用意されていないんですよね。。。
実際、ナビゲーションバーを非表示にしておきたい場合など、キーボードが表示されたタイミングを検知したい場合が多々あったため
よい方法はないかとググりました。Activityの高さの変化などを取得し、キーボードの状態変化を検知する方法等ありますが、
最も簡単な方法かと思われる素敵なライブラリが公開されていたので共有です。https://github.com/yshrsmz/KeyboardVisibilityEvent
このような素敵なライブラリが埋もれているのは勿体ないのでお勧めです。
- 投稿日:2020-08-04T16:13:46+09:00
【Kotlin】スクロールと連動させつつ動的にFABを表示したり隠したりする
はじめに
FloatingActionButtonを使うときはCoordinatorLayoutやRecyclerViewと一緒に使ったりして、スクロールに合わせて表示・非表示を切り替えるかと思います。
この場合は、レイアウトファイルのFABに「app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"」と書けばスクロールにあわせていい感じにやってくれます。
しかし、これだと何らかの理由でスクロールができなくなった時にFABが隠れたままになってしまうことがあります。どうするか
HideBottomViewOnScrollBehaviorにはslideUp/slideDownメソッドがあるのですが、protectedなメソッドのため外部から呼ぶことはできません。
なので継承したクラスでオーバーライドします。ListActivity.ktclass ListActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_list) fab = findViewById<FloatingActionButton>(R.id.fab) } class FabScrollBehavior<V:View>(context: Context?, attrs:AttributeSet?):HideBottomViewOnScrollBehavior<V>(context,attrs){ public override fun slideDown(child: V) { super.slideDown(child) } public override fun slideUp(child: V) { super.slideUp(child) } } }それと、app:layout_behaviorも自作したクラスに変更します。
activity_list.xml<androidx.coordinatorlayout.widget.CoordinatorLayout 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="com.test.ListActivity"> <androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent"/> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="match_parent" app:layout_behavior="com.test.ListActivity$FabScrollBehavior"<!--自作したクラスにする-- android:layout_gravity="bottom|end" android:layout_marginBottom="15sp" android:layout_marginEnd="15sp" android:src="@android:drawable/ic_input_add"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>あとは好きな時にslideUpやslideDownを呼ぶだけです。
ListActivity.ktclass ListActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_list) fab = findViewById<FloatingActionButton>(R.id.fab) } private fun showFab(){//FABを表示する val behavior = (fab.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? FabScrollBehavior?: return behavior.slideUp(fab) } class FabScrollBehavior<V:View>(context: Context?, attrs:AttributeSet?):HideBottomViewOnScrollBehavior<V>(context,attrs){ public override fun slideDown(child: V) { super.slideDown(child) } public override fun slideUp(child: V) { super.slideUp(child) } } }おしまい
FABにはhide()やshow()メソッドがあっても、HideBottomViewOnScrollBehaviorとは共存できないっぽく、今のところこれが一番楽でいい感じかなーと思いました。
なにかおかしなことしてたら教えて頂けると嬉しいです。
- 投稿日:2020-08-04T16:00:24+09:00
Androidアプリ開発探求記(その7)
概要
今回は、buildSrc を導入しつつ、build.gradle を build.gradle.kts に書き換えてみようと思います。
現行ソース
◆ build.gradle
buildscript { // 『この位置』でのバージョン情報定義は、結構他の多くのプロジェクトでも踏襲してるっぽい。 // // - mockk(https://github.com/mockk/mockk/blob/master/build.gradle)の場合は結構愚直にやってる。 // // - picasso(https://github.com/square/picasso/blob/master/build.gradle)の場合は // ext.versions = [..] と ext.deps = [..] に分けている。 // // - kotlinpoet(https://github.com/square/kotlinpoet/blob/master/buildSrc/src/main/java/Dependencies.kt)の場合は // buildSrc を利用して version, deps を定義している。picasso の方式を buildSrc に移行しただけっぽい感じですね。 // ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }◆ app/build.gradle
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.objectfanatics.chrono0015" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' }kts化&buildSrc 適用後
◆ build.gradle.kts
build.gradle.ktsbuildscript { repositories { google() jcenter() } dependencies { classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}") } } allprojects { repositories { google() jcenter() } } task("clean", Delete::class) { delete = setOf(rootProject.buildDir) }◆ app/build.gradle.kts
app/build.gradle.ktsplugins { id("com.android.application") // TODO: この extension って逆にわかりづらい気がする。 kotlin("android") id("kotlin-android-extensions") } android { compileSdkVersion(29) defaultConfig { applicationId = "com.objectfanatics.chrono0015" minSdkVersion(23) targetSdkVersion(29) versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } compileOptions { coreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } } dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.0.10") implementation(fileTree("dir" to "libs", "include" to listOf("*.jar"))) implementation("androidx.appcompat:appcompat:1.1.0") implementation("androidx.constraintlayout:constraintlayout:1.1.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}") testImplementation("junit:junit:4.12") androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") androidTestImplementation("androidx.test.ext:junit:1.1.1") }◆ Dependencies.kt
buildSrc/src/main/java/Dependencies.kt// - kotlinpoet(https://github.com/square/kotlinpoet/blob/master/buildSrc/src/main/java/Dependencies.kt)の場合は // buildSrc を利用して version, deps を定義している。picasso の方式を buildSrc に移行しただけっぽい感じですね。 object versions { const val kotlin = "1.3.72" } // TODO: 要検討 //object deps { //}懸念点
現在、以下のように PluginDependenciesSpec の extension を用いた個所があります。
plugins { id("com.android.application") // TODO: この extension って逆にわかりづらい気がする。 kotlin("android") id("kotlin-android-extensions") }しかし、個人的な感覚としては、以下のような id 指定のほうが分かりやすいような気がします。
plugins { id("com.android.application") // TODO: こっちのほうが分かりやすいように思える。 id("org.jetbrains.kotlin.android") id("kotlin-android-extensions") }extension を利用するメリットとしては、コードが短くなることや、コンパイルエラーにならない typo の発生を抑えるなどがあると思います。
しかし、下記 deps のような仕組みを設けてしまえば、IDEのコード補完により extension を利用するメリットはほぼ無くなるような気がします。
buildSrc/src/main/java/Dependencies.ktobject versions { const val kotlin = "1.3.72" } // TODO: 要検討 //object deps { //}deps 側での extension の利用に関しては、extension が buildSrc 側からデフォルトで参照できないことや、id で揃ったほうが右脳の処理負荷が圧倒的に少ない(おいら調べ)という点で、やはり、extension を利用するメリットを感じません。
まとめ
今回は、buildSrc を導入しつつ、build.gradle を build.gradle.kts に書き換えてみました。
次回は、前出の懸念点について対応してみようと思います。
- 投稿日:2020-08-04T14:35:06+09:00
Android アプリに広告(Google AdMob)入れて不労所得したい
きっかけ
最近私の巷で副業が流行っています。
そこで私も、「何か Web アプリでも開発して、広告収入を得てみたい!!」などと漠然と考えるようになりました。
個人開発者が作成しているとても魅力的な Web アプリを参考にしたり、自分がこれあったらいいなという物を考えて、いざ開発しよう!という段階にまで来ました。
しかし Web アプリは作って終わりではなく、サーバ設置やデータベース作成、ドメイン取得、その他セキュリティ設定、その他・・・などが必要になってくると知った私は
開発したものはすぐにリリースしたいのに、時間かかりそうだなー
一つ一つ攻略していくのに何かと時間かかりそうだなー
開発したものがそこまででもないのに伸びなかったら時間の無駄だなー
- ⇒ 本当はそんなことはなく、Web アプリのリリース手法が身につくので本来は自分にとってプラスであることは重々承知しています。。。
広告収入の仕組みがどんな感じなのか知りたいだけなのにそんなに時間かける必要性は?
などと思ってしまい、やる気がなくなってしまいました。
ただ、自分のモチベーション的に
広告収入なら何も Web アプリじゃなくても良いのでは?
Android アプリにも広告収入があったよな?
というか、Android アプリは apk 作成したらすぐリリースできるから、広告収入の仕組みが学べそう!
Android アプリは勉強のために一度リリースしたことがあるので、そこに広告を乗っけられそうですぐできる(自己都合)!
などと思い、せっかくだからこの機に Android アプリの広告収入に挑戦してみようと思ったのがきっかけです。
この記事について
この記事では以下のことについて説明します。
Android アプリに Google AdMob を導入する方法について
- ここではどのように実装したかのみを記述するだけなので、詳細を知りたい方は Google AdMob のチュートリアル・ドキュメントをご覧ください。
なお、以下のことについては説明いたしません。
Android アプリを GooglePlay でリリースする方法について
- Android Studio のユーザーガイド「アプリを公開する」や、ほかのわかりやすい記事を参考にしてください。
Google AdMob について
Google AdMob とは開発した Android アプリに広告を乗せ、容易に収益化できるサービスです。
無料の Android アプリを使用した方はわかると思いますが、アプリ画面の下のほうに見かけるバナー広告や、画面いっぱいに表示される動画広告などは、AdMob で簡単に実装できます。
なお、AdMob は Android アプリ上に表示された広告がクリックされると報酬を得ることができる「クリック報酬型広告」に分類されています。
一例
エミュレータのため、「テスト広告」と出ていますが、リリースすれば取れます。
参考:自作したカウンターアプリ
AdMob の始め方
簡単に述べると、AdMob を初めて利用する方が行う作業は以下の 2 点ととても少ないです。
AdMob に個人情報を登録
GooglePlay に公開するアプリに、AdMob 用の広告 ID を導入
以下ではその手順を確認していきます(基本はドキュメントに沿っていけば簡単にできます)。
1. AdMob の公式ドキュメント通りに沿って進める
初めて AdMob を利用する際は、個人情報の登録などを行います。
アプリを公開済みだった場合は、広告を乗せるアプリを選択することができるので、すぐに実装できます。
2. アプリに乗せる広告を以下から選択する
バナー広告(画面上下部に表示される長方形の広告。初心者に最適らしい。)
インタースティシャル広告(画面全体に表示される広告。画面が切り替わるタイミングなどに乗せるときに有効。)
ネイティブ広告(アプリのデザインに併せてカスタマイズが可能。上級者向け。)
リワード広告(短い動画を視聴し、ユーザーに報酬を進呈する広告。上級者向け。)
3. 載せる広告を選択後、AdMob を利用するための情報を開発したアプリに実装する
- 以下のような画面が表示されるので、これに沿って手順を進める。
アプリ ID 実装箇所
AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest ~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~> <application ~~~~~~~~ ~~~~~~~~ ~~~~~~~~> <!-- ここに AdMob のテンプレとアプリ ID を追加 --> <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-xxxxxxxxxxxxxxxx~xxxxxxxxxx" /> <activity ~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~> </activity> </application> </manifest>広告ユニット ID 実装箇所
※ バナー広告を載せる場合は、ほかのコンテンツやインタラクティブ要素に重ならないように配置するようにするのがポイントです。禁止事項に触れる可能性があります。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~> <TextView ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /> <Button ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /> <!-- ここに AdMob のテンプレと広告ユニット ID を追加 --> <com.google.android.gms.ads.AdView android:id="@+id/adView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" app:adSize="BANNER" app:adUnitId="ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0" /> </androidx.constraintlayout.widget.ConstraintLayout>4. ビルドして終了
ビルドして問題がなえれば終了です。
あとは作成したアプリを GooglePlay に公開してどのように広告が表示されるか確認してみましょう!
終わりに
初めて Android アプリに広告(AdMob)を導入してみましたが、実作業としては「 AdMob に登録」と作成したアプリに広告 ID を実装するだけで済みました。
とても簡単に広告って導入することができるのですね!
ただ AdMob の場合、広告がクリックされないとキャッシュを得られないため、今後はより面白い Android アプリを開発して、より多くの人々にダウンロードしてもらえるものを作っていきたいと思います!
また、Web アプリでも興味を引くことができるものを作成して、ゆくゆくは個人開発者としてステップアップしていきたいと思います。
- 投稿日:2020-08-04T13:03:02+09:00
Unity+Android で画像保存が出来ない。
Unity + Android で画像保存を確認しようとしたら
うまくいかなかった外部保存には以下のものを使用
https://github.com/yasirkula/UnityNativeGalleryそれでこの記事を見て解決。
どうやら、設定を何かしら変えないと保存できない模様。
細かいことは気にせず動いたので問題解決とする。
https://qiita.com/Katumadeyaruhiko/items/c2b9b4ccdfe51df4ad4apublic void SaveImage() { Sprite backCameraOn = Resources.Load<Sprite>("適当な画像"); Texture2D frontImage = createReadabeTexture2D( backCameraOn.texture ); NativeGallery.SaveImageToGallery( frontImage, "album", "bodygramFront"); } Texture2D createReadabeTexture2D(Texture2D texture2d) { RenderTexture renderTexture = RenderTexture.GetTemporary( texture2d.width, texture2d.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); Graphics.Blit(texture2d, renderTexture); RenderTexture previous = RenderTexture.active; RenderTexture.active = renderTexture; Texture2D readableTextur2D = new Texture2D(texture2d.width, texture2d.height); readableTextur2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); readableTextur2D.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(renderTexture); return readableTextur2D; }‘‘‘
- 投稿日:2020-08-04T12:35:52+09:00
物理アニメーションでRecyclerViewにオーバースクロールアニメーションを実装してみる
はじめに
「Android Studio のRecyclerViewをオーバースクロール(bouncing animation, ios風にスクロール)させたい」、こんなことを思った人は多いと思います。(このアニメーション何て言うのか分かんない...)かく言う自分もその一人です。
デフォルトのoverScrollMode
とかいう色が付くスクロールアニメーションってダサくね?って思ってました。初めてRecyclerViewを実装したときは、ios風にスクロール端のアニメーションを設定するにはどうするんだろう? なんかそういうAttributeでもあんだろ?
... そんな風に甘く考えてました。実際にやろうとして、すぐにAttributeがないことに気づき嘘やろ?ってなりました。
何だかよくわからんが変なアダプタ作ったり、scrollリスナ使ったり、ライブラリ使ったりして3日ほど頑張ってみたけど、結局出来なくて泣く泣く諦めた記憶があります。(recyclerView触りたてのヤツがやるには難易度高すぎw)それから少し時間がたちリベンジ。
検索していたらとても参考になる記事を発見したので、それを共有したいと思います。Ashu Tyagi氏によるBounce effect in RecyclerView
という天才的な記事。これに全て書いてあった!
ちなみに java, kotlinだと簡単そうで実装がめんどいこのオーバースクロールアニメーション、Flutter 触ったことがある方なら知ってると思いますが1行で書けます ( ´∀` )
頼むからbouncing scroll
的なAttribute 追加してくれ ...今回やること
はい、前置きが少し長くなりましたが、タイトルにあるようにオーバースクロールアニメーションを付けたRecyclerViewをやってみます。↓
一番下にGitHubを載せますので必要な方は参照してください。冒頭でも書いたとおり、基本的にはBounce effect in RecyclerViewをみれば実装は分かりますが、ちょこちょこ解説いれつつ見ていきたいと思います。
流れ
- まずスクロールのアニメーションには物理アニメーションである
Spring Animation
を使います。(公式ドキュメント)- RecylerViewのViewHolderにSpringAnimationオブジェクトをview holderとして持たせます。(recyclerViewのview holder一つ一つにアニメーションをセットします)
- RecyclerViewにEdgeEffectFactory をセットしてアニメーションを動かす。
依存関係
Android X の dynamicanimation を追加します。(2020/8月時点)
builde.gradleimplementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'stable ver1.0.0 がリリースされたのは2019/12 みたいです。
View Holderにアニメーション追加
RecyclerViewのview holderにspringAnimationオブジェクトを持たせます。RecyclerView全体にではなくview holder単位でアニメーションを設定していきます。
なお、今回recyclerViewの実装については触れません。(必要な方は最下部掲載のgithubをご覧ください)RecyclerAdapter.kt// view holder class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) { val content: TextView = view.content // view holderにspringAnimationオブジェクト追加 val springAnimY: SpringAnimation = SpringAnimation(itemView, SpringAnimation.TRANSLATION_Y) .setSpring(SpringForce().apply { finalPosition = 0f //ばねの静止位置の設定 dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY //ばねの減衰比(0~1f) stiffness = SpringForce.STIFFNESS_VERY_LOW //ばねの剛性(0~) }) }今回は
TRANSLATION_Y
でy軸方向のSpringとしてアニメーションさせますが、他にもROTATION
,SCALE
,ALPHA
などそれぞれX,Y,Z軸について存在します。(alphaのspringアニメーションの使いどころむずくねw)ここで、
SpringForce
のプロパティがfinalPosition
,dampingRatio
,stiffness
と3つほどあります。 これらのプロパティはアニメーションに使うばねの挙動を調整つするためのものです。
finalPosition
はアニメーションの最終的な終了位置を示すために初期化しておく必要があるようなものだと分かると思いますが、dampingRatio
,stiffness
は何?
それぞれ範囲は0~1f, 0~∞になっています。(もちろんfloatのビット数限界まで)翻訳かけてみたら、dampingRatio : 減衰比 、stiffness : 剛性と出ました。oh、なるほど。自分と同じ工学部の方なら聞き覚えがあるかもしれません。
簡単に言うと、
dampingRatio stiffness ばねのビヨーンとなる動きの止まりやすさに関係してきます ばねの強さのことで、引き戻すまたは反発の速さに関係します。 0だと全く振動せずすぐに止まり、1だと永遠に振動し続けます。 0だと引き戻す力が弱いのでひっぱても全然戻ってきません。大きい(100000f)と離すと一瞬で戻ってきます。 カスタムで指定もできますが、上記のコードの定数が今回は自然だと思うので考えたくない人はとりあえずコピペしてください。
物理アニメーション
今回は
SpringAnimation
(バネアニメーション)を使ったので減衰振動、バネ定数などによる物理アニメーションでしたが、他にもFlingAnimation
(投射アニメーション)、加速度、摩擦、水平投射などの物理アニメーションもあります。(公式ドキュメント)
名前からして実世界を意識したマテリアルデザインとかと相性がよさそうですね。edgeEffectFactoryのセット
つづいて、recyclerViewに
edgeEffectFactory
セットしていきます。edgeEffectFactoryを使えば、名前の通り、recyclerViewのスクロール端のエフェクトに関してカスタムすることができます。今回はedgeEffectFactoryを使って、recyclerViewのスクロール端で 先ほどview holderに追加したアニメーションを動作させます。
MainActivity.ktrecyclerView.adapter = myAdapter recyclerView.edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() { override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { return object : EdgeEffect(applicationContext) { //これから実装 // HERE } } }ちょっと無名オブジェクトがネストしていて分かりづらいですが、ここにEdgeEffectのメソッドを実装していきます。
以下に載せるのは上記のコードの HEREの部分に追加するコードです。
MainActivity(HEREの部分).ktval OVER_SCROLL_COEF = 0.4f // リスト端にスクロールしたときにどのくらいまでリスト外までスクロールさせるかを決める比率係数 val OVER_FLICK_COEF = 0.4f // リスト端にフリックしたときに程度リスト外までスクロールさせるかを決める比率係数 // リストの端に行ったときに呼び出される override fun onPull(deltaDistance: Float, displacement: Float) { super.onPull(deltaDistance, displacement) // deltaDistance 0~1f 前回からの変化した割合 // displacement 0~1f タップした位置の画面上の相対位置 val sign = if (direction == DIRECTION_BOTTOM) -1 else 1 val deltaY = sign * view.height * deltaDistance * OVER_SCROLL_COEF // view holderのアニメーションを移動距離に合わせて更新 for (i in 0 until view.childCount) { view.apply { val holder = getChildViewHolder(getChildAt(i)) as RecyclerAdapter.CustomViewHolder holder.springAnimY.cancel() holder.itemView.translationY += deltaY } } } // 指を離したとき override fun onRelease() { super.onRelease() // アニメーションスタート for (i in 0 until view.childCount) { view.apply { val holder = getChildViewHolder(getChildAt(i)) as RecyclerAdapter.CustomViewHolder holder.springAnimY.start() } } } // リストをフリックして画面端に行ったとき override fun onAbsorb(velocity: Int) { super.onAbsorb(velocity) val sign = if (direction == DIRECTION_BOTTOM) -1 else 1 val translationVelocity = sign * velocity * OVER_FLICK_COEF for (i in 0 until view.childCount) { view.apply { val holder = getChildViewHolder(getChildAt(i)) as RecyclerAdapter.CustomViewHolder holder.springAnimY .setStartVelocity(translationVelocity) .start() } } }
onPull
メソッドについて、少し注意すべきなのが、名前的にrecyclerViewがスクロールされるとコールされるように感じますが、実際にコールされるのはrecyclerViewの上端、下端などの端でさらにスクロールしようとした場合にコールされます。(普通にリストの真ん中くらいをスクロールしても呼ばれません。)
引数のdeltaDistance
はrecyclerView(画面上に表示されてるもののみ)のpixelの絶対位置です。なのでdeltaY
はスクロールによる移動距離を比率係数で調整しつつ求めています。
また、スクロール中はanimationをcancelすることで、そのアニメーションが終わっていなくても(オーバースクロール中でも)再び別のアニメーションプロパティを更新してアニメーションを動作させることができます。
これがないと、オーバースクロールしたときに、通常の位置にリストが戻ってくるまでスクロールすることができなくなります。
onRelease
メソッドはタップしてスクロールをはじめ、スクロールし終わって、指を離した瞬間にコールされます。このタイミングで、view holderのアニメーションを動作させます。
onAbsrob
メソッドは、recyclerViewをフリックしてrecyclerViewの上端、下端に行ったときにコールされます。引数のvelocity
はその時の速さで、その単位は[pixel/sec]となっています。
translationVelocity
で速度からアニメーションプロパティを更新します。こんな感じで、普通にスクロールしてスクロール端に到達したときと、フリックしてスクロール端に到達したときで別々にアニメーションを動作させる処理を書きます。
ちなみに冒頭で述べた記事ではview hoderを取得するのに、拡張関数を書いていました。
んー、エレガント!MainActivity.ktview.forEachVisibleHolder<RecyclerAdapter.CustomViewHolder> {holder-> holder.springAnimY .setStartVelocity(translationVelocity) .start() } fun <reified T : RecyclerView.ViewHolder> RecyclerView.forEachVisibleHolder( action: (T) -> Unit ) { for (i in 0 until childCount) { action(getChildViewHolder(getChildAt(i)) as T) } }個人的に拡張関数をゴリゴリ使っていく人は、経験あるプロって感じがする。自分はまだまだ...
これで完了!!
ビルドしてみてみると冒頭のようなスクロールアニメーションができてるはずです。今回のコードはこちらからどうぞ↓
GitHub
- 投稿日:2020-08-04T11:11:38+09:00
【Android/Kotlin】世界測地系を日本測地系に変換する
世界測地系と日本測地系
測地系とは緯度経度を表すときの基準になるものです。
(詳しくはググってください)
普段GoogleMap等で目にするのは世界測地系ですが、まれに日本測地系を使用する場合があります。
今回その例に遭遇したので、変換方法をメモしておこうと思います。ライブラリのインストール
使用するライブラリ→ Proj4J
// 測地系変換を実現するPROJライブラリ implementation 'org.osgeo:proj4j:0.1.0'測地系の変換
最後の
convertPoint
に変換後の緯度経度が入っています。val crsFactory = CRSFactory() // wgs84(世界測地系) val wgs84 = crsFactory.createFromParameters( null, "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs") // 日本測地系 val tokyo = crsFactory.createFromParameters( null, "+proj=longlat +ellps=bessel +towgs84=-146.336,506.832,680.254,0,0,0,0 +no_defs") val transformFactory = CoordinateTransformFactory() val transform: CoordinateTransform = transformFactory.createTransform(wgs84, tokyo) // 変換したい緯度経度 val targetPoint = ProjCoordinate(lng, lat) var convertPoint = ProjCoordinate() convertPoint = transform.transform(targetPoint, convertPoint)
- 投稿日:2020-08-04T04:08:44+09:00
Androidアプリ開発探求記(その6)
概要
現状のコードは JDK8 対応が中途半端なので、今回はきちんと JDK8 に対応してみようと思います。
やること
JDK8 対応する。
現行コード
app/build.gradleapply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.objectfanatics.chrono0015" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) // TODO: jdk8 は要検討。 implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' }JDK8対応方針
◆ kotlin
kotlin については、JDK8 を一部サポートするコードの入っているバージョンを利用しようと思います。
具体的には、
org.jetbrains.kotlin:kotlin-stdlib
をorg.jetbrains.kotlin:kotlin-stdlib-jdk8
に変更するだけです。詳細は Configuring Dependencies 参照のこと。
◆ desugaring
ソースコード上で java や kotlin を JDK8 に対応しても、アプリがサポート対象とする一部の実行環境に JDK8 の API が含まれていなという状況では
Build.VERSION.SDK_INT
のチェック等による条件分岐が必要となり、保守が難しくなります。そのため、この問題を Java 8+ API desugaring により対応します。
※詳細は Java 8+ API desugaring メモ 参照のこと。変更後のコード
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.objectfanatics.chrono0015" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' }まとめ
今回は JDK8 への対応を行いました。
build.gradle はある程度整理できた気がするので、次回は build.gradle の kts 化をしてみようと思います。
- 投稿日:2020-08-04T03:08:43+09:00
Androidアプリ開発探求記(その5)
概要
今回は、build.gradle について探求してみたいと思います。
やること
build.gradle を一通り確認してみようと思います。
現行コード
◆ build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } }◆ app/build.gradle
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.objectfanatics.chrono0015" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }コードを読み解く
一通り内容を確認して、コメントを入れてみます。
考察が必要な個所には
TODO: 要考察
というコメントを入れておきます。◆ build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { // 『この位置』でのバージョン情報定義は、結構他の多くのプロジェクトでも踏襲してるっぽい。 // // - mockk(https://github.com/mockk/mockk/blob/master/build.gradle)の場合は結構愚直にやってる。 // // - picasso(https://github.com/square/picasso/blob/master/build.gradle)の場合は // ext.versions = [..] と ext.deps = [..] に分けている。 // // - kotlinpoet(https://github.com/square/kotlinpoet/blob/master/buildSrc/src/main/java/Dependencies.kt)の場合は // buildSrc を利用して version, deps を定義している。picasso の方式を buildSrc に移行しただけっぽい感じですね。 // ext.kotlin_version = "1.3.72" // repositories は特に問題無し。 // https://developer.android.com/studio/build/dependencies#remote-repositories repositories { google() jcenter() } dependencies { // com.android.application plugin 等のために必要。 // https://developer.android.com/studio/build classpath "com.android.tools.build:gradle:4.0.1" // kotlin-android plugin 等のために必要。 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } // 全プロジェクトに下記 repositories を適用するのに問題はなさげ。 allprojects { // repositories は特に問題無し。 // https://developer.android.com/studio/build/dependencies#remote-repositories repositories { google() jcenter() } } // root project には clean という default のタスクは存在しない。 // そのため、この場に clean タスクを記述している。 task clean(type: Delete) { delete rootProject.buildDir }◆ app/build.gradle
// android の build のために必要 apply plugin: 'com.android.application' // kotlin で書かれた android project を build するには kotlin-android plugin が必要 // https://kotlinlang.org/docs/reference/using-gradle.html#targeting-android apply plugin: 'kotlin-android' // TODO: 要考察(1): org.jetbrains.kotlin:kotlin-stdlib への依存があれば、削除しても動作する。しかし、そうでなければビルド時にエラーになる。 // https://developer.android.com/kotlin/ktx apply plugin: 'kotlin-android-extensions' android { // お約束なので問題無し。 compileSdkVersion 29 // お約束なので問題無し。 defaultConfig { applicationId "com.objectfanatics.chrono0015" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" // JUnit 4 のテストクラス群を利用するため、AndroidJUnitRunner をデフォルトの test instrumentation runner としてセットしている。 // instrumented unit test とは実機(やエミュレータ)上で実行される unit test のこと。 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } // Androidアプリ開発探求記(その2)で追加した。 // https://qiita.com/beyondseeker/items/2b1fba123efbab6c04ae#-jdk%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) // TODO: 要考察(1): kotlin-android-extensions が利用されていれば、削除しても動作する。しかし、そうでなければビルド時にエラーになる。 // TODO: jdk8 は要検討。 // Cannot access built-in declaration 'kotlin.Unit'. Ensure that you have a dependency on the Kotlin standard library implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // TODO: 要考察(2): 削除しても動作する。 // https://developer.android.com/kotlin/ktx // implementation 'androidx.core:core-ktx:1.3.1' // appcompat は事実上不可欠な存在。Android Studio から生成されたプロジェクト中のコードでも利用されている。 implementation 'androidx.appcompat:appcompat:1.1.0' // constraint layout は事実上不可欠と言っていいと思う。Android Studio から生成されたプロジェクト中のコードでも利用されている。 implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // 通常の unit test から利用される JUnit 4 のクラス群への依存。※ org.junit.Test 等 testImplementation 'junit:junit:4.12' // instrumented unit test から利用される JUnit 4 のクラス群への依存。※ AndroidJUnit4, ActivityScenarioRule 等 androidTestImplementation 'androidx.test.ext:junit:1.1.1' // Espresso testing framework への依存。 // Espresso testing framework は UI テストのためのフレームワーク。 // @see https://developer.android.com/training/testing/ui-testing/espresso-testing androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }考察
1. kotlinの依存関係
☆ 関連コード
要考察(1)のコードを以下に抜粋します:
// TODO: 要考察(1): org.jetbrains.kotlin:kotlin-stdlib への依存があれば、削除しても動作する。しかし、そうでなければビルド時にエラーになる。 apply plugin: 'kotlin-android-extensions' dependencies { // TODO: 要考察(1): kotlin-android-extensions が利用されていれば、削除しても動作する。しかし、そうでなければビルド時にエラーになる。 // Cannot access built-in declaration 'kotlin.Unit'. Ensure that you have a dependency on the Kotlin standard library implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" }☆ 依存関係の確認方法
以下のように依存関係を確認してみます。
$ ./gradlew app:dependencies --configuration debugCompileClasspath☆ 両方削除
kotlin-android-extensions と org.jetbrains.kotlin:kotlin-stdlib 両方の記述を削除した場合は、kotlin 関連の依存は検出されませんでした。
ビルド失敗するのは当然ですね。
☆ ライブラリだけ記述
以下のような依存関係が検出されました。(kotlinで行フィルタ)
debugCompileClasspath - Compile classpath for compilation 'debug' (target (androidJvm)). +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72 +--- org.jetbrains.kotlin:kotlin-stdlib:{strictly 1.3.72} -> 1.3.72 (c) +--- org.jetbrains.kotlin:kotlin-stdlib-common:{strictly 1.3.72} -> 1.3.72 (c)☆ プラグインだけ記述
以下のような依存関係が検出されました。(kotlinで行フィルタ)
debugCompileClasspath - Compile classpath for compilation 'debug' (target (androidJvm)). +--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72 | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72 | \--- org.jetbrains:annotations:13.0 +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 (*) +--- androidx.core:core-ktx:1.3.1 | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.3.72 (*) +--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:{strictly 1.3.72} -> 1.3.72 (c) +--- org.jetbrains.kotlin:kotlin-stdlib:{strictly 1.3.72} -> 1.3.72 (c) +--- org.jetbrains.kotlin:kotlin-stdlib-common:{strictly 1.3.72} -> 1.3.72 (c)☆ プラグインだけでいいのか?
事足りていて、困らないのであれば、ライブラリ側の記述はなくても実害は無いと思いますし、コードの保守コストが減るというメリットもあるのでプラグインだけ記述するという選択肢もあるかもしれません。しかし、plugin の副作用とも言えるものなので、明示的に指定してあげるのが正しい気がします。
また、jdk8 を指定するような場合は記述が必要ですが、この点に関しては、次回 jdk8 対応することとして、今は放置することにします。
2. ktxの依存関係
☆ 関連コード
要考察(2)のコードを以下に抜粋します:
// TODO: 要考察(2): 削除しても動作する。 // https://developer.android.com/kotlin/ktx // implementation 'androidx.core:core-ktx:1.3.1'☆ 考察
普通に ktx のコードを使ってないというだけのことですよね。たぶん。
使うときに追加すればよいということで、削除しちゃうことにします。現時点のコード
まとめ
今回は、build.gradle の内容確認とある程度の整理をしてみました。
次回は、TODO として残された jdk8 対応について考えてみようと思います。
- 投稿日:2020-08-04T02:00:44+09:00
【Flutter】DateTimeを「何日前」「何分前」など現在時刻との差分で表示させる方法
今回は、flutterで、「何日前」、「何分前」など現在時刻からの差分で表示していきます。
現在時刻より「〜日前」「〜分前」の時間をとって来きて画面に表示する
ソースコード
main.dart// 現在の時刻 final DateTime now = DateTime.now(); class Date extends StatelessWidget { // 30秒前の時刻 final DateTime thirtySecondsAgo = now.add(Duration(seconds: 30) * -1); // 30分前の時刻 final DateTime thirtyMinutesAgo = now.add(Duration(minutes: 30) * -1); // 5時間前の時刻 final DateTime fiveHoursAgo = now.add(Duration(hours: 30) * -1); // 30日前の時刻 final DateTime thirtyDaysAgo = now.add(Duration(days: 30) * -1); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: < Widget[ // ⓵ Text(DateFormat('yyyy/MM/dd(E) HH:mm:ss').format(now)), // ② Text(DateFormat('yyyy/MM/dd(E) HH:mm:ss').format(thirtySecondsAgo)), // ③ Text(DateFormat('yyyy/MM/dd(E) HH:mm:ss').format(thirtyMinutesAgo)), // ④ Text(DateFormat('yyyy/MM/dd(E) HH:mm:ss').format(fiveHoursAgo)), // ⑤ Text(DateFormat('yyyy/MM/dd(E) HH:mm:ss').format(thirtyDaysAgo)), ]), ), ); } }ひとまず、ここまでで使った技術を紹介
DateTime.add()
FlutterのDateTimeクラスには、addというメソッドが用意せれていてこれを用いて指定した期間、前か後の時刻(DateTime)をとってこれる。
例えば、30秒後なら以下のようにすればとって来れます。
final thirtySecondsAfter = DateTime.add(Duration(seconds: 30))
addの引数にDurationを渡してあげれば簡単にとってこれます。
Flutter の公式ドキュメント 「DateTime class」
https://api.flutter.dev/flutter/dart-core/DateTime-class.htmlDateTimeをフォーマットを指定してStringに変換
まずは、pub.devからintlというライブラリを使えるようにします。
pubspec.yamldependencies: flutter: sdk: flutter intl: ^0.16.1main.dartimport 'package:flutter/material.dart'; import 'package:intl/intl.dart';これでintlが使えるようになりました
次に、フォーマットを指定してStringに変換していきます。
例:
DateFormat('yyyy/MM/dd(E)').format(DateTime date)
・このようにDataFormatの引数に指定するFormatを渡す。
⬇️
・formatの引数にStringに変換したい時刻(DateTime)を渡してあげる
⬇️
・これをTextの引数に渡すと表示できる参考にした記事
・pub.devのintlパッケージ
https://pub.dev/packages/intl
・flutterでDateTimeとStringの変換方法とTimeZoneとLocale
https://qiita.com/ko2ic/items/bd0d20d72c66e8231c5c現在時刻との差分を表示する
ソースコード
main.dartfinal DateTime now = DateTime.now(); class Date extends StatelessWidget { final List<DateTime> dates = [ // 30秒前の時刻 now.add(Duration(seconds: 30) * -1), // 30分前の時刻 now.add(Duration(minutes: 30) * -1), // 5時間前 now.add(Duration(hours: 30) * -1), // 30日前 now.add(Duration(days: 30) * -1) ]; @override Widget build(BuildContext context) { final difference = dates.map((date) => Text(fromAtNow(date))).toList(); return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: difference), ), ); } } String fromAtNow(DateTime date) { final Duration difference = DateTime.now().difference(date); final int sec = difference.inSeconds; if (sec >= 60 * 60 * 24) { return '${difference.inDays.toString()}日前'; } else if (sec >= 60 * 60) { return '${difference.inHours.toString()}時間前'; } else if (sec >= 60) { return '${difference.inMinutes.toString()}分前'; } else { return '$sec秒前'; } }この方法を解説
実際にはここの部分で、現在時刻との差分をStringに変換しています。
String fromAtNow(DateTime date) { final Duration difference = DateTime.now().difference(date); final int sec = difference.inSeconds; if (sec >= 60 * 60 * 24) { return '${difference.inDays.toString()}日前'; } else if (sec >= 60 * 60) { return '${difference.inHours.toString()}時間前'; } else if (sec >= 60) { return '${difference.inMinutes.toString()}分前'; } else { return '$sec秒前'; } }手順
1. まずは、DateTimeクラスのdifferenceメソッドを使って差分をDurationで返します。
例では、DataTime.nowとdateの差分を取ってきました。例:
final Duration difference = DateTime.now().difference(date);
2. DurationのinSecondsメソッドで差分(Duration)を秒数(int)に変換してあげる
例:
final int sec = difference.inSeconds;
3. 60秒以下の場合は、〜秒前のように場合分けする
例:
if(sec < 60) {return '$sec秒前';}
以上!!
参考になる記事
Flutter の公式ドキュメント 「DateTime class」
https://api.flutter.dev/flutter/dart-core/DateTime-class.html
・pub.devのintlパッケージ
https://pub.dev/packages/intl
・flutterでDateTimeとStringの変換方法とTimeZoneとLocale
https://qiita.com/ko2ic/items/bd0d20d72c66e8231c5c
・一週間前、または、現在の日時からn日後、n日前の日付を取得する - C#プログラミング
https://www.ipentec.com/document/csharp-get-a-week-ago-datetime