The biggest problem is that I can't formulate my question, so I couldn't even search for the solution. I don't know if I'm going in the right direction at all.
I would to be able to swipe up to drag(?) a new layout into the visible area. This should follow the user's finger movements up and down. This means that the user starts to swipe and the layout is gradually moved into place. When a threshold is reached, the indented element can become final after the finger is released. If the user changes his mind on the fly and move your finger backwards and release it below the threshold, the new layout will not be installed after you release your finger. It would be even cool if the new layout would not only be made visible by its movement, but also its transparency would change from completely transparent to opaque to make it more expressive.
So far I have tried the following code:
fragment_bottom_sheet.xml (the layout that can be dragged into the visible area during the swipe. Now it only has two buttons to make it look like something, and it has a white background colour to make it look like)
<LinearLayout 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="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@android:color/white"
>
<button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 1" />
<button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 2" />
</LinearLayout>
BottomSheetFragment.kt (The corresponding kotlin class)
class BottomSheetFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_bottom_sheet, container, false)
}
}
I created a custom FrameLayout that can handle the swipe event as described above. Importantly, it only takes up a small area of the screen - see the green area in the diagram - but it should still work when I swipe my finger off it. So it's just the starting trigger point for the swipe.
SwipeFrameLayout.kt
class SwipeFrameLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var startY = 0f
private var isSwiping = false
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
val bottomSwipeArea = findViewById<View>(R.id.bottom_swipe_area)
val rect = rect()
bottomSwipeArea.getGlobalVisibleRect(rect)
if (rect.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
startY = ev.y
isSwiping = true
return true
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (isSwiping) {
if (event.action == MotionEvent.ACTION_UP) {
isSwiping = false
}
return handleSwipe(event)
}
return super.onTouchEvent(event)
}
private fun handleSwipe(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val deltaY = event.y - startY
val threshold = (context as MainActivity).screenHeight / 3
if (deltaY < 0 && abs(deltaY) <= threshold) {
val interpolator = abs(deltaY) / threshold
val translateY = (context as MainActivity).initialY - deltaY * interpolator
(context as MainActivity).container.y = translateY
Log.d(
this::class.java.toString(),
"handleSwipe: move" + translateY + ", " + (context as MainActivity).container.height
)
}
return true
}
MotionEvent.ACTION_UP -> {
val deltaY = event.y - startY
val threshold = (context as MainActivity).screenHeight / 3
if (abs(deltaY) >= threshold) {
(context as MainActivity).supportFragmentManager.beginTransaction()
.replace(R.id.container, (context as MainActivity).bottomSheetFragment)
.commit()
Log.d(this::class.java.toString(), "handleSwipe: replace")
} else {
(context as MainActivity).container.y = (context as MainActivity).initialY
Log.d(this::class.java.toString(), "handleSwipe: hide")
}
return true
}
else -> return false
}
}
}
MainActivity.kt calculates a few values just for now
class MainActivity : AppCompatActivity() {
lateinit var bottomSheetFragment: BottomSheetFragment
lateinit var bottomSwipeArea: SwipeFrameLayout
lateinit var container: FrameLayout
var initialY = 0f
var screenHeight = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomSheetFragment = BottomSheetFragment()
bottomSwipeArea = findViewById(R.id.bottom_swipe_area)
container = findViewById(R.id.container)
screenHeight = resources.displayMetrics.heightPixels
container.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
container.viewTreeObserver.removeOnGlobalLayoutListener(this)
initialY = container.y
}
})
}
}
Finally, the main layout in the activity_main.xml file
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".app.main.MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:orientation="vertical"
android:padding="@dimen/card_margin">
<TextView
style="@style/h1_headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/card_margin"
android:text="@string/app_name" />
<!-- some irrelevant TextView and Button -->
</LinearLayout>
</ScrollView>
</FrameLayout>
<my.example.app.main.SwipeFrameLayout
android:background="@color/green"
android:id="@+id/bottom_swipe_area"
android:layout_width="match_parent"
android:layout_height="75dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:context=".app.main.MainActivity" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
According to the logs, the swap happens, but it doesn't show up on the display, and the movement doesn't seem to happen properly, as I would it to (proportionally follow the length of the swipe up to the threshold)