/* * Copyright (C) 2024 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.intentresolver.widget import android.content.Context import android.graphics.Rect import android.util.AttributeSet import android.view.View import android.widget.LinearLayout import androidx.core.view.ScrollingView import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop /** * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to * orchestrate content preview scrolling. It expects one [LinearLayout] child with * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child * will be made scrollable (it is expected to be a content preview view). */ class ChooserNestedScrollView : NestedScrollView, TargetListScrollStateQuery { 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 var shouldScrollOnRectangleOnScreenRequest = true var requestChildFocusPredicate: (View?, View?) -> Boolean = DefaultChildFocusPredicate override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val content = getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected") require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" } require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { "Expected to have an exact width" } val lp = content.layoutParams ?: error("LayoutParams is missing") val contentWidthSpec = getChildMeasureSpec( widthMeasureSpec, paddingLeft + content.marginLeft + content.marginRight + paddingRight, lp.width, ) val contentHeightSpec = getChildMeasureSpec( heightMeasureSpec, paddingTop + content.marginTop + content.marginBottom + paddingBottom, lp.height, ) content.measure(contentWidthSpec, contentHeightSpec) if (content.childCount > 1) { // We expect that the first child should be scrollable up val child = content.getChildAt(0) val height = MeasureSpec.getSize(heightMeasureSpec) + child.measuredHeight + child.marginTop + child.marginBottom content.measure( contentWidthSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)), ) } setMeasuredDimension( MeasureSpec.getSize(widthMeasureSpec), minOf( MeasureSpec.getSize(heightMeasureSpec), paddingTop + content.marginTop + content.measuredHeight + content.marginBottom + paddingBottom, ), ) } override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { // let the parent scroll super.onNestedPreScroll(target, dx, dy, consumed, type) // scroll ourselves, if recycler has not scrolled val delta = dy - consumed[1] if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) { val preScrollY = scrollY scrollBy(0, delta) consumed[1] += scrollY - preScrollY } } private var flingingTargetListChild: TargetListScrollStateQuery? = null override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean { flingingTargetListChild = target as? TargetListScrollStateQuery return super.onNestedPreFling(target, velocityX, velocityY).also { flingingTargetListChild = null } } override val isAtTop: Boolean get() = !canScrollVertically(-1) && (flingingTargetListChild?.isAtTop ?: true) override fun onRequestChildFocus(child: View?, focused: View?) { if (requestChildFocusPredicate(child, focused)) { // When a child view is requesting a focus, it will trigger two parent's method calls: // `onRequestChildFocus` followed by `requestChildRectangleOnScreen`. Both calls makes // `NestedScrollView` scroll to the child and this logic is added to ignore some of // them. Unfortunately, `requestChildRectangleOnScreen` does not have enough information // to easily determine which view is receiving the focus (it would require to find a // descended view that matches the given rectangle) thus // `shouldScrollOnRectangleOnScreenRequest` is used as a heuristic to ignore the call. shouldScrollOnRectangleOnScreenRequest = true super.onRequestChildFocus(child, focused) } else { shouldScrollOnRectangleOnScreenRequest = false } } override fun requestChildRectangleOnScreen( child: View, rectangle: Rect?, immediate: Boolean, ): Boolean { val shouldScroll = shouldScrollOnRectangleOnScreenRequest shouldScrollOnRectangleOnScreenRequest = true return shouldScroll && super.requestChildRectangleOnScreen(child, rectangle, immediate) } companion object { val DefaultChildFocusPredicate: (View?, View?) -> Boolean = { _, _ -> true } } }