Demo for `ViewMotionValue` Bug: 389637975 Test: manual Change-Id: Ifc40355006185467163c1d04b58dc95eceb48a64
diff --git a/samples/MotionMechanics/Android.bp b/samples/MotionMechanics/Android.bp index 7e948f7..4dda196 100644 --- a/samples/MotionMechanics/Android.bp +++ b/samples/MotionMechanics/Android.bp
@@ -37,6 +37,8 @@ "androidx.compose.runtime_runtime", "androidx.navigation_navigation-compose", "androidx.window_window", + "androidx-constraintlayout_constraintlayout", + "androidx.appcompat_appcompat", ], kotlincflags: ["-Xjvm-default=all"],
diff --git a/samples/MotionMechanics/app/AndroidManifest.xml b/samples/MotionMechanics/app/AndroidManifest.xml index 70b27a1..e6b48a3 100644 --- a/samples/MotionMechanics/app/AndroidManifest.xml +++ b/samples/MotionMechanics/app/AndroidManifest.xml
@@ -32,5 +32,17 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <activity + android:name="com.android.mechanics.demo.ViewDemoActivity" + android:theme="@style/Theme.AppCompat.NoActionBar" + android:windowSoftInputMode="adjustResize" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> </manifest>
diff --git a/samples/MotionMechanics/res/layout/view_demo.xml b/samples/MotionMechanics/res/layout/view_demo.xml new file mode 100644 index 0000000..3dfee6d --- /dev/null +++ b/samples/MotionMechanics/res/layout/view_demo.xml
@@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<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:id="@+id/rootLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="80dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + tools:context=".MainActivity"> + + <Spinner + android:id="@+id/dropdown" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + /> + + <View + android:id="@+id/box" + android:layout_width="20dp" + android:layout_height="20dp" + android:background="#FF0000" + app:layout_constraintTop_toBottomOf="@id/dropdown" + app:layout_constraintStart_toStartOf="parent" + + android:layout_marginTop="40dp" + /> + + <SeekBar + android:id="@+id/slider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="50dp" + app:layout_constraintTop_toBottomOf="@id/box" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/ViewDemoActivity.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/ViewDemoActivity.kt new file mode 100644 index 0000000..12abfb5 --- /dev/null +++ b/samples/MotionMechanics/src/com/android/mechanics/demo/ViewDemoActivity.kt
@@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mechanics.demo + +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.SeekBar +import android.widget.Spinner +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.mechanics.spec.Mapping +import com.android.mechanics.spec.MotionSpec +import com.android.mechanics.spec.buildDirectionalMotionSpec +import com.android.mechanics.spring.SpringParameters +import com.android.mechanics.view.DistanceGestureContext +import com.android.mechanics.view.ViewMotionValue + +class ViewDemoActivity : AppCompatActivity() { + + private lateinit var slider: SeekBar + private lateinit var box: View + private lateinit var rootLayout: ConstraintLayout + private lateinit var dropdown: Spinner + private var specs = createMotionSpecs(100f) + + private val gestureContext by lazy { DistanceGestureContext.create(this) } + private val motionValue by lazy { ViewMotionValue(0f, gestureContext, MotionSpec.Empty) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.view_demo) + + slider = findViewById(R.id.slider) + box = findViewById(R.id.box) + rootLayout = findViewById(R.id.rootLayout) + dropdown = findViewById(R.id.dropdown) + dropdown.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + val key = checkNotNull(parent?.adapter?.getItem(position) as String) + + motionValue.spec = checkNotNull(specs[key]) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + + var maxRange = -1f + + slider.viewTreeObserver.addOnGlobalLayoutListener { + val newWidth = slider.width.toFloat() - slider.thumb.bounds.width() + if (maxRange != newWidth) { + val percentage = slider.progress.toFloat() / slider.max.toFloat() + maxRange = newWidth + + slider.max = maxRange.toInt() + slider.progress = (maxRange * percentage).toInt() + + specs = createMotionSpecs(maxRange) + + dropdown.adapter = + ArrayAdapter(this, android.R.layout.simple_spinner_item, specs.keys.toList()) + .apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + } + } + + slider.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar?, + progress: Int, + fromUser: Boolean, + ) { + if (fromUser) { + gestureContext.dragOffset = progress.toFloat() + motionValue.input = progress.toFloat() + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + } + ) + + motionValue.addUpdateCallback { + val layoutParams = box.layoutParams as ConstraintLayout.LayoutParams + layoutParams.leftMargin = motionValue.output.toInt() + box.layoutParams = layoutParams + } + } + + override fun onDestroy() { + super.onDestroy() + motionValue.dispose() + } + + fun createMotionSpecs(maxRange: Float): Map<String, MotionSpec> { + + return mapOf( + "empty" to MotionSpec.Empty, + "toggle" to + MotionSpec( + buildDirectionalMotionSpec(DefaultSpring, Mapping.Zero) { + constantValue(breakpoint = maxRange / 2f, value = maxRange) + } + ), + ) + } + + companion object { + val DefaultSpring = SpringParameters(700f, 0.8f) + } +}