1

I would like to add content to an app that starts at about 70% down vertically and can be scrolled upwards to cover the top 70% views.

I thought of using two children ConstraintLayout's inside a parent ConstraintLayout - the two children would be on top of each other. One would contain the views that would populate the first 70% of the screen while the other would contain a NestedScrollView which has an invisible <View> that takes up 70% of the height and then the additional content that can be scrolled up.

I'm facing a problem with marking the 70% spot - using a Guideline inside the NestedScrollView isn't working because the %s are fluid (it matches to 70% of the content inside the NestedScrollView instead of 70% of the viewable screen). Using a Guideline outside the NestedScrollView doesn't work because well... constraints have to be siblings to compile.

How can I accomplish this?

           <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/parentLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >

            <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/firstConstraintLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/red5F"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent">
                // A bunch of content that should fill up the first 70% of the screen and be covered by the overlay if user scrolls
            </androidx.constraintlayout.widget.ConstraintLayout>
            <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/overlayConstraintLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent">
              
                <androidx.core.widget.NestedScrollView
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:fillViewport="true"
                    android:id="@+id/scrollView"
                    app:layout_constraintLeft_toLeftOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent">
                    <androidx.constraintlayout.widget.ConstraintLayout
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:id="@+id/overlayInnerLayout">
                        <androidx.constraintlayout.widget.Guideline
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:id="@+id/verticalGuidelineOverlay"
                            android:orientation="horizontal"
                            app:layout_constraintGuide_percent="0.7"/>
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="0dp"
                            android:id="@+id/spacerView"
                            app:layout_constraintTop_toTopOf="parent"
                            app:layout_constraintBottom_toTopOf="@id/verticalGuidelineOverlay"
                            app:layout_constraintLeft_toLeftOf="parent"/>
                        // More content here that the user could scroll upwards that would start at the 70% point and eventually cover the entire screen.
                     </ConstraintLayout>
           </NestedScrollView>
      </ConstraintLayout>
    </ConstraintLayout>

Video w/example here: https://i.stack.imgur.com/twK0H.jpg

user1114
  • 1,071
  • 2
  • 15
  • 33

3 Answers3

3

Try out this method,

<?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:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/firstConstraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent">


</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/overlayConstraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:fillViewport="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:weightSum="1">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0.7"
                android:orientation="horizontal">

                <RelativeLayout
                    android:id="@+id/transparentView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"/>
                <View
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"

                android:orientation="vertical"
                android:layout_weight="0.3" />

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/bg">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="20sp"
                android:text="@string/lorem_ipsum"
                tools:ignore="MissingConstraints"
                android:textSize="18sp"/>
            </RelativeLayout>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

set 70% height programatically using layoutParams

val transparentView = findViewById<RelativeLayout>(R.id.transparentView)

    val metrics = DisplayMetrics()
    windowManager.defaultDisplay.getMetrics(metrics)

    val height = Math.min(metrics.widthPixels, metrics.heightPixels) //height


    val params = transparentView.layoutParams
    params.height = (height * 70) / 70
    transparentView.layoutParams = params

you will get the required result : enter link description here

Yahya M
  • 392
  • 3
  • 13
  • 1
    If you notice the shared gif of the OP, when it's scrolled up to the most; the bottom part of the main layout is showing which is not the same as yours – Zain Aug 06 '21 at 15:05
0

Remove guidelines and use a view like this as a spacer view. It's height constrained to be 1.15 of it's width. You can change it around a littile to get what you want

<View
    android:id="@+id/spacerView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="1:1.15"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

Also just as an advice

  1. you're not supposed to use match_parent in ConstraintLayout, use 0dp and constraint it to both sides.
  2. Top layout can be replaced with FrameLayout, cause you don't really use any constraints
Ariorick
  • 11
  • 3
  • Seems a bit tricky with different screen sizes / orientations. Is there an easier way? e.g. set the content to start below a specific view? – user1114 Aug 01 '21 at 22:37
  • It’s gonna be fine with different screen sizes, cause all devices are 16:9 (10) nowadays. For a landscape orientation to look good you would probably still have to do a separate xml and put it in layouts-land – Ariorick Aug 01 '21 at 22:44
  • @user114 i doubt you can do percentages with constraint layout in this case. Another way is to set spacer view size programmatically 1) Get screen size and multiply by 0.7 https://stackoverflow.com/a/31377616/7270189 2) Set view size https://stackoverflow.com/a/5257851/7270189 – Ariorick Aug 01 '21 at 22:51
  • Thanks. I know this isn't the same question so feel free to ignore, but is there a way to get this effect and also set the bottom part to always begin after the top part ends? The text for the top part may be dynamic causing it to have different sizes – user1114 Aug 01 '21 at 23:35
  • I guess you can measure your top part by getting it's height/margins and using this value or spacer view. Btw can you mark the answer as correct? c: – Ariorick Aug 02 '21 at 06:27
0

You can use a customized BottomSheetDialogFragment that has a theme of Theme_Translucent_NoTitleBar, and change the y value of the root layout of the dialog whenever the user drags it up or down.

class MyDialogFragment(height: Int) : BottomSheetDialogFragment(), View.OnTouchListener {

    private val outsideWindowHeight = height

    private val rootLayout by lazy {
        requireView().findViewById<LinearLayout>(R.id.dialog_root)
    }

    private var oldY = 0
    private var baseLayoutPosition = 0
    private var defaultViewHeight = 0
    private var isClosing = false
    private var isScrollingUp = false
    private var isScrollingDown = false

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(
            requireContext(),
            android.R.style.Theme_Translucent_NoTitleBar
        )
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        val view: View = inflater.inflate(
            R.layout.fragment_dialog, container,
            false
        )
        view.setBackgroundResource(R.drawable.rounded_background)

        (dialog as BottomSheetDialog).apply {
            setCancelable(false)
            behavior.peekHeight =
                (outsideWindowHeight * 0.3).toInt()  // Minimum height of the BottomSheet is 30% of the root layout (to leave the 70% to the main layout)
        }

        return view
    }


    @SuppressLint("ClickableViewAccessibility")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        rootLayout.setOnTouchListener(this)

    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        // Get finger position on screen
        val y = event!!.rawY.toInt()

        // Switch on motion event type
        when (event.action and MotionEvent.ACTION_MASK) {
        
            MotionEvent.ACTION_DOWN -> {
                // save default base layout height
                defaultViewHeight = rootLayout.height

                oldY = y
                baseLayoutPosition = rootLayout.y.toInt()
            }
            
            MotionEvent.ACTION_UP -> {
                // If user was doing a scroll up
                if (isScrollingUp) {
                    // Reset baselayout position
                    rootLayout.y = 0f
                    // We are not in scrolling up anymore
                    isScrollingUp = false
                }

                // If user was doing a scroll down
                if (isScrollingDown) {
                    // Reset baselayout position
                    rootLayout.y = 0f
//                     Reset base layout size
                    rootLayout.layoutParams.height = defaultViewHeight
                    rootLayout.requestLayout()
                    // We are not in scrolling down anymore
                    isScrollingDown = false
                }
            }


            MotionEvent.ACTION_MOVE -> {

                if (rootLayout.y <= -100) {
                    return true
                }

                if (!isClosing) {
                    val currentYPosition = rootLayout.y.toInt()

                    // If we scroll up
                    if (oldY > y) {
                        // First time android rise an event for "up" move
                        if (!isScrollingUp) {
                            isScrollingUp = true
                        }

                        rootLayout.y = rootLayout.y + (y - oldY)
                        
                    } else {

                        // First time android rise an event for "down" move
                        if (!isScrollingDown) {
                            isScrollingDown = true
                        }


                        // change position because view anchor is top left corner
                        rootLayout.y = rootLayout.y + (y - oldY)
                        rootLayout.requestLayout()
                    }

                    // Update position
                    oldY = y
                }
            }
        }
        return true
    }
}

fragment_dialog.xml (Nothing fancy):

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dialog_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_bottom_sheet_heading"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/dp_56"
        android:layout_marginStart="@dimen/dp_16"
        android:layout_marginEnd="@dimen/dp_16"
        android:gravity="center"
        android:text="@string/bottom_sheet_option_heading"
        android:textColor="@android:color/black"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_btn_add_photo_camera"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_48"
        android:layout_marginStart="@dimen/dp_16"
        android:layout_marginEnd="@dimen/dp_16"
        android:backgroundTint="@android:color/white"
        android:drawableStart="@drawable/ic_camera_alt_black_24dp"
        android:drawableLeft="@drawable/ic_camera_alt_black_24dp"
        android:drawablePadding="@dimen/dp_32"
        android:drawableTint="@color/md_bottom_sheet_text_color"
        android:gravity="start|center_vertical"
        android:text="@string/bottom_sheet_option_camera"
        android:textColor="@color/md_bottom_sheet_text_color"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_btn_add_photo_gallery"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_marginStart="@dimen/dp_16"
        android:layout_marginEnd="@dimen/dp_16"
        android:backgroundTint="@android:color/white"
        android:drawableStart="@drawable/ic_insert_photo_black_24dp"
        android:drawableLeft="@drawable/ic_insert_photo_black_24dp"
        android:drawablePadding="@dimen/dp_32"
        android:drawableTint="@color/md_bottom_sheet_text_color"
        android:gravity="start|center_vertical"
        android:text="@string/bottom_sheet_option_gallery"
        android:textColor="@color/md_bottom_sheet_text_color"
        android:textSize="16sp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="@dimen/md_bottom_sheet_separator_top_margin"
        android:layout_marginBottom="@dimen/dp_8"
        android:background="@color/grayTextColor" />

    <TextView
        android:id="@+id/tv_btn_remove_photo"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_48"
        android:layout_marginStart="@dimen/dp_16"
        android:layout_marginEnd="@dimen/dp_16"
        android:backgroundTint="@android:color/white"
        android:drawableStart="@drawable/ic_delete_black_24dp"
        android:drawableLeft="@drawable/ic_delete_black_24dp"
        android:drawablePadding="@dimen/dp_32"
        android:drawableTint="@color/md_bottom_sheet_text_color"
        android:gravity="start|center_vertical"
        android:text="@string/bottom_sheet_option_remove_photo"
        android:textColor="@color/md_bottom_sheet_text_color"
        android:textSize="16sp" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_material"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Material button"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium" />


    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/longText1"
        android:textColor="@color/white"
        android:textSize="22sp" />


</LinearLayout>

And send the height of the root ViewGroup of the main layout to the dialog in the main activity:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val root = findViewById<ConstraintLayout>(R.id.root)

        root.viewTreeObserver.addOnGlobalLayoutListener(object :
            OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                root.viewTreeObserver
                    .removeOnGlobalLayoutListener(this)

                val dialogFragment = MyDialogFragment(root.height)
                dialogFragment.show(supportFragmentManager, "dialog_tag")

            }
        })

    }
}

Preview:

Zain
  • 37,492
  • 7
  • 60
  • 84