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() が設定されています。

  1. class ScrollerSampleView : FrameLayout {  
  2.   
  3.     constructor(context: Context) : super(context)  
  4.     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)  
  5.     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
  6.         context,  
  7.         attrs,  
  8.         defStyleAttr  
  9.     )  
  10.   
  11.     private val size = (100 * resources.displayMetrics.density).toInt()  
  12.   
  13.     private val targetView: View = View(context).apply {  
  14.         layoutParams = LayoutParams(size, size)  
  15.         setBackgroundColor(Color.RED)  
  16.     }  
  17.     private val textView: TextView = TextView(context).apply {  
  18.         layoutParams = LayoutParams(  
  19.             LayoutParams.WRAP_CONTENT,  
  20.             LayoutParams.WRAP_CONTENT  
  21.         )  
  22.     }  
  23.   
  24.     private val scroller = OverScroller(context)  
  25.   
  26.     init {  
  27.         addView(targetView)  
  28.         addView(textView)  
  29.     }  
  30.   
  31.     fun scroll(dx: Int, dy: Int, duration: Int, friction: Float) {  
  32.         scroller.setFriction(friction)  
  33.   
  34.         // scroll の前に今のアニメーションを止める  
  35.         scroller.forceFinished(true)  
  36.   
  37.         targetView.translationX = 0f  
  38.         targetView.translationY = 0f  
  39.   
  40.         val startX = 0  
  41.         val startY = 0  
  42.   
  43.         // アニメーションを開始  
  44.         scroller.startScroll(  
  45.             startX,   // scroll の開始位置 (X)  
  46.             startY,   // scroll の開始位置 (Y)  
  47.             dx,       // 移動する距離、正の値だとコンテンツが左にスクロールする (X)  
  48.             dy,       // 移動する距離、正の値だとコンテンツが左にスクロールする (Y)  
  49.             duration  // スクロールにかかる時間 [milliseconds]  
  50.         )  
  51.   
  52.         // これにより computeScroll() が呼ばれる  
  53.         postInvalidateOnAnimation()  
  54.     }  
  55.   
  56.     fun fling(velocityX: Int, velocityY: Int, overX: Int, overY: Int, friction: Float) {  
  57.         scroller.setFriction(friction)  
  58.   
  59.         // fling の前に今のアニメーションを止める  
  60.         scroller.forceFinished(true)  
  61.   
  62.         targetView.translationX = 0f  
  63.         targetView.translationY = 0f  
  64.   
  65.         val startX = 0  
  66.         val startY = 0  
  67.   
  68.         val minX = 0  
  69.         val maxX = 800  
  70.   
  71.         val minY = 0  
  72.         val maxY = 800  
  73.   
  74.         // アニメーションを開始  
  75.         scroller.fling(  
  76.             startX,      // scroll の開始位置 (X)  
  77.             startY,      // scroll の開始位置 (Y)  
  78.             velocityX,   // fling の初速 [px/sec] (X)  
  79.             velocityY,   // fling の初速 [px/sec] (Y)  
  80.             minX,        // X の最小値. minX - overX まで移動し、minX 未満のところは overfling 中になる  
  81.             maxX,        // X の最大値. maxX + overX まで移動し、maxX を超えたところは overfling 中になる  
  82.             minY,        // Y の最小値. minY - overY まで移動し、minY 未満のところは overfling 中になる  
  83.             maxY,        // Y の最大値. maxY + overY まで移動し、maxY を超えたところは overfling 中になる  
  84.             overX,       // overfling の範囲 (X). overfling の範囲は両端に適用される  
  85.             overY        // Overfling の範囲 (Y). overfling の範囲は両端に適用される  
  86.         )  
  87.   
  88.         // これにより computeScroll() が呼ばれる  
  89.         postInvalidateOnAnimation()  
  90.     }  
  91.   
  92.     override fun computeScroll() {  
  93.         super.computeScroll()  
  94.   
  95.         // computeScrollOffset() の戻り値が true == まだアニメーション中  
  96.         if (scroller.computeScrollOffset()) {  
  97.             textView.text = """  
  98.                 currVelocity: ${scroller.currVelocity}  
  99.                 currX: ${scroller.currX}  
  100.                 currY: ${scroller.currY}  
  101.                 startX: ${scroller.startX}  
  102.                 startY: ${scroller.startY}  
  103.                 finalX: ${scroller.finalX}  
  104.                 finalY: ${scroller.finalY}  
  105.                 isFinished: ${scroller.isFinished}  
  106.                 isOverScrolled: ${scroller.isOverScrolled}  
  107.             """.trimIndent()  
  108.   
  109.             targetView.translationX = scroller.currX.toFloat()  
  110.             targetView.translationY = scroller.currY.toFloat()  
  111.   
  112.             // アニメーション中なので再度呼ぶ  
  113.             postInvalidateOnAnimation()  
  114.         }  
  115.     }  
  116. }  
速度を 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 があるとします。
  1. @Module  
  2. object AppModule {  
  3.   
  4.     @Provides  
  5.     fun provideMyApi(): MyApi {  
  6.         ...  
  7.     }  
  8. }  
これを引数にとる Fragment があります。Dagger に生成をまかせたいのでコンストラクタに @Inject をつけます。
  1. class MainFragment @Inject constructor(private val api: MyApi) : Fragment() {  
  2.   
  3.     ...  
  4. }  
Fragment の Map Multibindings 用の MapKey を用意します。
  1. @Target(  
  2.     AnnotationTarget.FUNCTION,  
  3.     AnnotationTarget.PROPERTY_GETTER,  
  4.     AnnotationTarget.PROPERTY_SETTER  
  5. )  
  6. @Retention(AnnotationRetention.RUNTIME)  
  7. @MapKey  
  8. annotation class FragmentKey(val value: KClass<out Fragment>)  
用意した MapKey を使って MainFragment を Multibindings に追加します。
  1. @Module  
  2. interface FragmentModule {  
  3.   
  4.     @Binds  
  5.     @IntoMap  
  6.     @FragmentKey(MainFragment::class)  
  7.     fun bindMainFragment(fragment: MainFragment): Fragment  
  8. }  
Fragment の Multibindings を引数に取る FragmentFactory を用意します。
  1. class MyFragmentFactory @Inject constructor(  
  2.     private val providers: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>  
  3. ) : FragmentFactory() {  
  4.   
  5.     override fun instantiate(classLoader: ClassLoader, className: String): Fragment {  
  6.         val found = providers.entries.find { className == it.key.name }  
  7.             ?: throw IllegalArgumentException("unknown model class $className")  
  8.   
  9.         val provider = found.value  
  10.   
  11.         try {  
  12.             @Suppress("UNCHECKED_CAST")  
  13.             return provider.get()  
  14.         } catch (e: Exception) {  
  15.             return super.instantiate(classLoader, className)  
  16.         }  
  17.     }  
  18. }  
用意した MyFragmentFactory を取得するためのメソッドを Component に用意します。
  1. @Component(modules = [AppModule::class, FragmentModule::class])  
  2. interface AppComponent {  
  3.   
  4.     fun fragmentFactory(): MyFragmentFactory  
  5. }  
  6.   
  7. class MyApplication : Application() {  
  8.   
  9.     lateinit var appComponent: AppComponent  
  10.   
  11.     override fun onCreate() {  
  12.         super.onCreate()  
  13.   
  14.         appComponent = DaggerAppComponent.builder()  
  15.             .build()  
  16.     }  
  17. }  
supportFragmentManager.fragmentFactory に Component から取得した MyFragmentFactory をセットします。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         supportFragmentManager.fragmentFactory = (application as MyApplication).appComponent  
  7.             .fragmentFactory()  
  8.   
  9.         setContentView(R.layout.activity_main)  
  10.     }  
  11. }  
activity_main.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <fragment xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/mainFragment"  
  4.     android:name="net.yanzm.sample.MainFragment"  
  5.     android:layout_width="match_parent"  
  6.     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 になります。
  1. class SimpleDragView : FrameLayout {  
  2.   
  3.     constructor(context: Context) : super(context)  
  4.     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)  
  5.     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
  6.         context,  
  7.         attrs,  
  8.         defStyleAttr  
  9.     )  
  10.   
  11.     private val targetView: View  
  12.   
  13.     private var velocityTracker: VelocityTracker? = null  
  14.   
  15.     init {  
  16.         val size = (100 * resources.displayMetrics.density).toInt()  
  17.         targetView = View(context).apply {  
  18.             layoutParams = LayoutParams(size, size).apply {  
  19.                 gravity = Gravity.CENTER  
  20.             }  
  21.             setBackgroundColor(Color.RED)  
  22.         }  
  23.         addView(targetView)  
  24.     }  
  25.   
  26.     private var lastVelocityX = 0f  
  27.     private var lastVelocityY = 0f  
  28.   
  29.     override fun onTouchEvent(ev: MotionEvent): Boolean {  
  30.         when (ev.actionMasked) {  
  31.             MotionEvent.ACTION_DOWN -> {  
  32.                 velocityTracker?.clear()  
  33.                 velocityTracker = velocityTracker ?: VelocityTracker.obtain()  
  34.                 velocityTracker?.addMovement(ev)  
  35.             }  
  36.             MotionEvent.ACTION_MOVE -> {  
  37.                 velocityTracker?.let {  
  38.                     it.addMovement(ev)  
  39.                     val pointerId: Int = ev.getPointerId(ev.actionIndex)  
  40.                     it.computeCurrentVelocity(1000)  
  41.                     lastVelocityX = it.getXVelocity(pointerId)  
  42.                     lastVelocityY = it.getYVelocity(pointerId)  
  43.                 }  
  44.             }  
  45.             MotionEvent.ACTION_UP -> {  
  46.                 velocityTracker?.let {  
  47.                     ObjectAnimator  
  48.                         .ofPropertyValuesHolder(  
  49.                             targetView,  
  50.                             PropertyValuesHolder.ofFloat(  
  51.                                 View.TRANSLATION_X,  
  52.                                 lastVelocityX / 4  
  53.                             ),  
  54.                             PropertyValuesHolder.ofFloat(  
  55.                                 View.TRANSLATION_Y,  
  56.                                 lastVelocityY / 4  
  57.                             )  
  58.                         )  
  59.                         .apply {  
  60.                             addListener(object : AnimatorListenerAdapter() {  
  61.                                 override fun onAnimationEnd(animation: Animator?) {  
  62.                                     super.onAnimationEnd(animation)  
  63.                                     targetView.translationX = 0f  
  64.                                     targetView.translationY = 0f  
  65.                                 }  
  66.                             })  
  67.                         }  
  68.                         .setDuration(500)  
  69.                         .start()  
  70.                 }  
  71.   
  72.                 velocityTracker?.recycle()  
  73.                 velocityTracker = null  
  74.             }  
  75.             MotionEvent.ACTION_CANCEL -> {  
  76.                 velocityTracker?.recycle()  
  77.                 velocityTracker = null  
  78.             }  
  79.         }  
  80.         return true  
  81.     }  
  82. }  



2020年5月7日木曜日

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

以下のような @RunWith(AndroidJUnit4::class) を使わない Unit Test は問題なく動くのですが、
  1. class HogeTest {  
  2.   
  3.     @Test  
  4.     fun test() {  
  5.         val listener = mock<(Boolean) -> Unit>()  
  6.   
  7.         ...  
  8.   
  9.         verify(listener)(false)  
  10.     }  
  11. }  
次のように @RunWith(AndroidJUnit4::class) をつけるとエラーが発生します。
  1. @RunWith(AndroidJUnit4::class)  
  2. class HogeTest {  
  3.   
  4.     @Test  
  5.     fun test() {  
  6.         val listener = mock<(Boolean) -> Unit>()  
  7.   
  8.         ...  
  9.   
  10.         verify(listener)(false)  
  11.     }  
  12. }  

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 を定義すると動きます。
  1. @RunWith(AndroidJUnit4::class)  
  2. class HogeTest {  
  3.   
  4.     private interface Callback : (Boolean) -> Unit  
  5.   
  6.     @Test  
  7.     fun test() {  
  8.         val listener = mock<Callback>()  
  9.   
  10.         ...  
  11.   
  12.         verify(listener)(false)  
  13.     }  
  14. }  


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