2020年5月29日金曜日

Scroller を使う

Scroller というのは、スクロール時のアニメーションを実現するための x,y 位置を計算してくれるクラスです。

ScrollerOverScroller が用意されています。 OverScroller は行き過ぎて戻ってくるようなアニメーションができます。

Scroller にはアニメーションを開始するメソッドとして が用意されています。

使い方はこんな感じです。
  • 1. scroller.forceFinished() でアニメーションを止める
  • 2. scroller.fling() または scroller.startScroll() でアニメーションを開始する
  • 3. View.postInvalidateOnAnimation() を呼ぶ。これを呼ぶと View.computeScroll() が呼ばれる
  • 4. View.computeScroll() で scroller.computeScrollOffset() を呼ぶ。戻り値が true の場合アニメーションが終わっていないということ
  • 5. scroller.currX, scroller.currY を使って View の位置などを変える
setFriction() で摩擦を設定できます。デフォルトは ViewConfiguration.getScrollFriction() が設定されています。

class ScrollerSampleView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val size = (100 * resources.displayMetrics.density).toInt() private val targetView: View = View(context).apply { layoutParams = LayoutParams(size, size) setBackgroundColor(Color.RED) } private val textView: TextView = TextView(context).apply { layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ) } private val scroller = OverScroller(context) init { addView(targetView) addView(textView) } fun scroll(dx: Int, dy: Int, duration: Int, friction: Float) { scroller.setFriction(friction) // scroll の前に今のアニメーションを止める scroller.forceFinished(true) targetView.translationX = 0f targetView.translationY = 0f val startX = 0 val startY = 0 // アニメーションを開始 scroller.startScroll( startX, // scroll の開始位置 (X) startY, // scroll の開始位置 (Y) dx, // 移動する距離、正の値だとコンテンツが左にスクロールする (X) dy, // 移動する距離、正の値だとコンテンツが左にスクロールする (Y) duration // スクロールにかかる時間 [milliseconds] ) // これにより computeScroll() が呼ばれる postInvalidateOnAnimation() } fun fling(velocityX: Int, velocityY: Int, overX: Int, overY: Int, friction: Float) { scroller.setFriction(friction) // fling の前に今のアニメーションを止める scroller.forceFinished(true) targetView.translationX = 0f targetView.translationY = 0f val startX = 0 val startY = 0 val minX = 0 val maxX = 800 val minY = 0 val maxY = 800 // アニメーションを開始 scroller.fling( startX, // scroll の開始位置 (X) startY, // scroll の開始位置 (Y) velocityX, // fling の初速 [px/sec] (X) velocityY, // fling の初速 [px/sec] (Y) minX, // X の最小値. minX - overX まで移動し、minX 未満のところは overfling 中になる maxX, // X の最大値. maxX + overX まで移動し、maxX を超えたところは overfling 中になる minY, // Y の最小値. minY - overY まで移動し、minY 未満のところは overfling 中になる maxY, // Y の最大値. maxY + overY まで移動し、maxY を超えたところは overfling 中になる overX, // overfling の範囲 (X). overfling の範囲は両端に適用される overY // Overfling の範囲 (Y). overfling の範囲は両端に適用される ) // これにより computeScroll() が呼ばれる postInvalidateOnAnimation() } override fun computeScroll() { super.computeScroll() // computeScrollOffset() の戻り値が true == まだアニメーション中 if (scroller.computeScrollOffset()) { textView.text = """ currVelocity: ${scroller.currVelocity} currX: ${scroller.currX} currY: ${scroller.currY} startX: ${scroller.startX} startY: ${scroller.startY} finalX: ${scroller.finalX} finalY: ${scroller.finalY} isFinished: ${scroller.isFinished} isOverScrolled: ${scroller.isOverScrolled} """.trimIndent() targetView.translationX = scroller.currX.toFloat() targetView.translationY = scroller.currY.toFloat() // アニメーション中なので再度呼ぶ postInvalidateOnAnimation() } } } 速度を 1000 [px/sec], 2000 [px/sec], 3000 [px/sec], 4000 [px/sec]、摩擦を ViewConfiguration.getScrollFriction(), ViewConfiguration.getScrollFriction() / 2、overfling 範囲を 0, 200 で上記の fling() を呼んだ結果が次の動画です。





摩擦を半分にすると同じ速度でも遠くまで移動し、overfling 範囲をつけると行き過ぎて戻ってくるようになります。



2020年5月27日水曜日

Dagger に Fragment と FragmentFactory の生成をまかせる

Master of Dagger の改定版にも入れる予定です。ただいま鋭意執筆中です。もう少々お待ちください。


ViewModelFactory と同じような感じで FragmentFactory および Fragment の生成をまかせることができます。

オブジェクトグラフに MyApi があるとします。 @Module object AppModule { @Provides fun provideMyApi(): MyApi { ... } } これを引数にとる Fragment があります。Dagger に生成をまかせたいのでコンストラクタに @Inject をつけます。 class MainFragment @Inject constructor(private val api: MyApi) : Fragment() { ... } Fragment の Map Multibindings 用の MapKey を用意します。 @Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class FragmentKey(val value: KClass<out Fragment>) 用意した MapKey を使って MainFragment を Multibindings に追加します。 @Module interface FragmentModule { @Binds @IntoMap @FragmentKey(MainFragment::class) fun bindMainFragment(fragment: MainFragment): Fragment } Fragment の Multibindings を引数に取る FragmentFactory を用意します。 class MyFragmentFactory @Inject constructor( private val providers: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>> ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { val found = providers.entries.find { className == it.key.name } ?: throw IllegalArgumentException("unknown model class $className") val provider = found.value try { @Suppress("UNCHECKED_CAST") return provider.get() } catch (e: Exception) { return super.instantiate(classLoader, className) } } } 用意した MyFragmentFactory を取得するためのメソッドを Component に用意します。 @Component(modules = [AppModule::class, FragmentModule::class]) interface AppComponent { fun fragmentFactory(): MyFragmentFactory } class MyApplication : Application() { lateinit var appComponent: AppComponent override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent.builder() .build() } } supportFragmentManager.fragmentFactory に Component から取得した MyFragmentFactory をセットします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager.fragmentFactory = (application as MyApplication).appComponent .fragmentFactory() setContentView(R.layout.activity_main) } } activity_main.xml <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="net.yanzm.sample.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" />


2020年5月21日木曜日

VelocityTracker の使い方

VelocityTracker はタッチイベントの速度計算を簡単にするためのクラスです。Fling など速度がジェスチャーの構成要素になっているものに対して便利です。

VelocityTracker.obtain() でインスタンスを取得します。
addMovement(ev) で MotionEvent を追加し、速度を取得するときは computeCurrentVelocity(int units) または computeCurrentVelocity(int units, float maxVelocity) を呼んだ後に getXVelocity(), getYVelocity() を呼びます。
obtain() で取得したインスタンスは不要になった時点で recycle() を呼びましょう。

computeCurrentVelocity() で maxVelocity を渡さない場合は Float.MAX_VALUE が使われます。 computeCurrentVelocity() で渡す units は getXVelocity(), getYVelocity() で取得する velocity の単位になります。1 を指定した場合は pixels per millisecond、1000 を渡した場合は pixels per second になります。 class SimpleDragView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val targetView: View private var velocityTracker: VelocityTracker? = null init { val size = (100 * resources.displayMetrics.density).toInt() targetView = View(context).apply { layoutParams = LayoutParams(size, size).apply { gravity = Gravity.CENTER } setBackgroundColor(Color.RED) } addView(targetView) } private var lastVelocityX = 0f private var lastVelocityY = 0f override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { velocityTracker?.clear() velocityTracker = velocityTracker ?: VelocityTracker.obtain() velocityTracker?.addMovement(ev) } MotionEvent.ACTION_MOVE -> { velocityTracker?.let { it.addMovement(ev) val pointerId: Int = ev.getPointerId(ev.actionIndex) it.computeCurrentVelocity(1000) lastVelocityX = it.getXVelocity(pointerId) lastVelocityY = it.getYVelocity(pointerId) } } MotionEvent.ACTION_UP -> { velocityTracker?.let { ObjectAnimator .ofPropertyValuesHolder( targetView, PropertyValuesHolder.ofFloat( View.TRANSLATION_X, lastVelocityX / 4 ), PropertyValuesHolder.ofFloat( View.TRANSLATION_Y, lastVelocityY / 4 ) ) .apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) targetView.translationX = 0f targetView.translationY = 0f } }) } .setDuration(500) .start() } velocityTracker?.recycle() velocityTracker = null } MotionEvent.ACTION_CANCEL -> { velocityTracker?.recycle() velocityTracker = null } } return true } }


2020年5月7日木曜日

mockito-kotlin で lambda を mock + @RunWith(AndroidJUnit4::class) のときは work around が必要

以下のような @RunWith(AndroidJUnit4::class) を使わない Unit Test は問題なく動くのですが、 class HogeTest { @Test fun test() { val listener = mock<(Boolean) -> Unit>() ... verify(listener)(false) } } 次のように @RunWith(AndroidJUnit4::class) をつけるとエラーが発生します。 @RunWith(AndroidJUnit4::class) class HogeTest { @Test fun test() { val listener = mock<(Boolean) -> Unit>() ... verify(listener)(false) } }
org.mockito.exceptions.base.MockitoException:
ClassCastException occurred while creating the mockito mock :
class to mock : 'kotlin.jvm.functions.Function1', loaded by classloader : 'sun.misc.Launcher$AppClassLoader@18b4aac2'
created class : 'kotlin.jvm.functions.Function1$MockitoMock$1350680399', loaded by classloader : 'net.bytebuddy.dynamic.loading.MultipleParentClassLoader@7a2a2c83'
proxy instance class : 'kotlin.jvm.functions.Function1$MockitoMock$1350680399', loaded by classloader : 'net.bytebuddy.dynamic.loading.MultipleParentClassLoader@7a2a2c83'
instance creation by : ObjenesisInstantiator


この場合クッションになる interface を定義すると動きます。 @RunWith(AndroidJUnit4::class) class HogeTest { private interface Callback : (Boolean) -> Unit @Test fun test() { val listener = mock<Callback>() ... verify(listener)(false) } }

参考 : https://github.com/nhaarman/mockito-kotlin/issues/272