Home > front end >  Android - Detecting when a view's constraints have been fully resolved/loaded (please help)
Android - Detecting when a view's constraints have been fully resolved/loaded (please help)

Time:07-16

EDIT: I HAVE FOUND THE SOLUTION TO THIS AFTER 2 DAYS OF NON-STOP DEBUGGING. THANKS EVERYONE!

Please make sure to read my edit for further description... IF anyone can even suggest me the slightest recommendation... I wil be graeteful... I am in desperate need of help :(, this bug is the strangest thing I've encountered in my life

I have quite a complex problem to do with Android views,

I am creating a paint application, and I have two views: a transparent background view and the pixel art board.

For both views, I want the height and width to be calculated off of the distance between view A and B:

enter image description here

Instead of calculating the distance between these two views, I simply 'constraint' a view in the middle like so, and then extract its height by using its measuredHeight property (and yes, you could also calculate the distance between view A and B in the code, but my problem still remains when I try that):

enter image description here

Now, here's the XML code:

<?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"
    android:background="@color/fragment_background_color_daynight"
    tools:context=".activities.canvas.CanvasActivity">
    <View
        android:id="@ id/activityCanvas_topView"
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:background="@color/fragment_background_color_daynight"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.therealbluepandabear.pixapencil.customviews.colorswitcherview.ColorSwitcherView
        android:id="@ id/activityCanvas_colorSwitcherView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        app:isPrimarySelected="false"
        app:layout_constraintBottom_toBottomOf="@ id/activityCanvas_colorPickerRecyclerView"
        app:layout_constraintEnd_toEndOf="@ id/activityCanvas_topView"
        app:layout_constraintTop_toTopOf="@ id/activityCanvas_colorPickerRecyclerView"
        app:primaryColor="@android:color/holo_green_dark"
        app:secondaryColor="@color/black" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@ id/activityCanvas_colorPickerRecyclerView"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:orientation="horizontal"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="@ id/activityCanvas_topView"
        app:layout_constraintEnd_toStartOf="@ id/activityCanvas_colorSwitcherView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@ id/activityCanvas_primaryFragmentHost"
        tools:listitem="@layout/color_picker_layout" />

    <FrameLayout
        android:id="@ id/activityCanvas_distanceContainer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@ id/activityCanvas_tabLayout"
        app:layout_constraintEnd_toEndOf="@ id/activityCanvas_primaryFragmentHost"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/activityCanvas_topView" />

    <com.google.android.material.card.MaterialCardView
        android:id="@ id/fragmentOuterCanvas_canvasFragmentHostCardViewParent"
        style="@style/activityCanvas_canvasFragmentHostCardViewParent_style"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:elevation="1dp"
        app:layout_constraintBottom_toTopOf="@ id/activityCanvas_tabLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/activityCanvas_topView">
        <!-- At runtime, the width and height will be calculated -->
       <com.therealbluepandabear.pixapencil.customviews.transparentbackgroundview.TransparentBackgroundView
            android:id="@ id/activityCanvas_transparentBackgroundView"
            android:layout_width="0dp"
            android:layout_height="0dp" />
    </com.google.android.material.card.MaterialCardView>

    <com.google.android.material.tabs.TabLayout
        android:id="@ id/activityCanvas_tabLayout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:tabStripEnabled="false"
        app:layout_constraintBottom_toTopOf="@ id/activityCanvas_viewPager2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">
        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/activityCanvas_tab_tools_str" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/activityCanvas_tab_filters_str" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/activityCanvas_tab_color_palettes_str" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/activityCanvas_tab_brushes_str" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@ id/activityCanvas_viewPager2"
        android:layout_width="0dp"
        android:layout_height="110dp"
        app:layout_constraintBottom_toBottomOf="@ id/activityCanvas_primaryFragmentHost"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@ id/activityCanvas_coordinatorLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <FrameLayout
        android:id="@ id/activityCanvas_primaryFragmentHost"
        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" />
</androidx.constraintlayout.widget.ConstraintLayout>

Of course, when it comes to calculating, I thought it would be best practise to utilize AndroidX's OneShotPreDrawListener, like so:

OneShotPreDrawListener.add(binding.root) {
    binding.activityCanvasTransparentBackgroundView!!.setViewWidth(binding.activityCanvasDistanceContainer!!.measuredHeight)
    binding.activityCanvasTransparentBackgroundView!!.setViewHeight(binding.activityCanvasDistanceContainer!!.measuredHeight)
}

Now, for some reason, the result looks like so:

enter image description here

Why is this the case!

I did some debugging, and when I log the height of view C, I get the following:

enter image description here

This is wrong. So, as an experiment, I added a GlobalLayoutListener to detect when exactly the view's constraints get resolved:

binding.activityCanvasDistanceContainer?.viewTreeObserver?.addOnGlobalLayoutListener( object : ViewTreeObserver.OnGlobalLayoutListener {
    override fun onGlobalLayout() {
        Log.d("M_LOG", binding.activityCanvasDistanceContainer?.measuredHeight.toString())
    }
})

Result:

enter image description here

So, after the first couple of times it's 438, until after the 4th/5th time it shoots up to 1000.

I am really confused why this is happening, as I want to run the event when the constraints have been fully resolved and calculated, but using OneShotPreDrawListener (or any other alternative) is just running the event when the view has been drawn, but not yet when it has been positioned properly.

I am confused what to do, so any help would be appreciated as to how I can run an event when the view's constraints have been fully calculated.

Edit for Chetichamp:

Hello,

I have debugged this for hours and I think I can reproduce this error and tell you in what scenario it occurs and in which scenario it does not.

Basically in my app, prior to activity creation, I have a fragment called NewProjectFragment, which looks like so:

enter image description here

Here's the code for when the 'Done' button is pressed:

binding.fragmentNewCanvasDoneButton.setOnClickListener {
    checkForTitleError()
    checkForWidthError()
    checkForHeightError()

    if (!invalidTitle && !invalidWidth && !invalidHeight) {
        try {
            val title =
                binding.fragmentNewCanvasProjectTitleTextInputEditText.text.toString()
            val widthValue: Int =
                binding.fragmentNewCanvasWidthTextInputEditText.text.toString().toInt()
            val heightValue: Int =
                binding.fragmentNewCanvasHeightTextInputEditText.text.toString().toInt()

            if (widthValue   heightValue >= 2000 && (requireActivity() as MainActivity).showLargeCanvasSizeWarning) {
                val frameLayout: FrameLayout =
                    [email protected]?.layoutInflater?.inflate(
                        R.layout.dont_show_large_canvas_warning_again_checkbox,
                        requireView().findViewById(android.R.id.content),
                        false
                    )
                            as FrameLayout
                val checkBox = frameLayout.getChildAt(0) as MaterialCheckBox

                requireActivity().showDialog(
                    getString(R.string.generic_warning_in_code_str),
                    getString(R.string.dialog_large_canvas_warning_text_in_code_str),
                    getString(R.string.dialog_large_canvas_warning_positive_button_text_in_code_str),
                    { _, _ ->
                        if (checkBox.isChecked) {
                            (requireActivity() as MainActivity).showLargeCanvasSizeWarning =
                                false

                            with((requireActivity() as MainActivity).sharedPreferenceObject.edit()) {
                                putBoolean(
                                    StringConstants.Identifiers.SHARED_PREFERENCE_SHOW_LARGE_CANVAS_SIZE_WARNING_IDENTIFIER,
                                    (requireActivity() as MainActivity).showLargeCanvasSizeWarning
                                )
                                apply()
                            }
                        }

                        caller.onDoneButtonPressed(
                            title,
                            widthValue,
                            heightValue,
                            paramSpotLightInProgress
                        )
                    },
                    getString(R.string.dialog_unsaved_changes_negative_button_text_in_code_str),
                    { _, _ ->
                    },
                    frameLayout
                )
            } else {
                caller.onDoneButtonPressed(
                    title,
                    widthValue,
                    heightValue,
                    paramSpotLightInProgress
                )
            }
        } catch (exception: Exception) {
            HapticFeedbackWrapper.performHapticFeedback(binding.fragmentNewCanvasDoneButton)
        }
    } else {
        HapticFeedbackWrapper.performHapticFeedback(binding.fragmentNewCanvasDoneButton)
    }
}

As you can see, it has a listener, so the code for the listener, which is in MainActivity, is like so (maybe this is causing the issue? and I just don't need a listener like this? Idk if you think this is why I get the issue tell me bro):

fun MainActivity.extendedOnDoneButtonPressed(projectTitle: String, width: Int, height: Int, spotLightInProgress: Boolean) {
    startActivity(
        Intent(this, CanvasActivity::class.java)
            .putExtra(StringConstants.Extras.PROJECT_TITLE_EXTRA, projectTitle)
            .putExtra(StringConstants.Extras.WIDTH_EXTRA, width)
            .putExtra(StringConstants.Extras.HEIGHT_EXTRA, height)
            .putExtra(StringConstants.Extras.SPOTLIGHT_IN_PROGRESS_EXTRA, spotLightInProgress)
    )
}

Now, what I come to the conclusion is that all of this extra work is causing a delay, because when you simply tap on a preexisting project, we can see that the intent is much simpler:

fun MainActivity.extendedOnCreationTapped(param: PixelArt) {
    startActivity(
        Intent(this, CanvasActivity::class.java)
            .putExtra(StringConstants.Extras.INDEX_EXTRA, pixelArtData.indexOf(param))
            .putExtra(StringConstants.Extras.PROJECT_TITLE_EXTRA, param.title))
}

With a simple intent like so, the problem doesn't get reproduced, and it sizes properly.

What I relaized is that the work done in NewProject fragment is causing a delay, and when I simply scrap out the work and perfom a simple intent, the problem is 'fixed'. I don't know how to fix this but hopefully it can help with finding a solution.

Debugging even further

When I debug the issue even further, I notice something strange. The measuredHeight of the root layout jumps up by one thousand:

enter image description here

This is not observed when the creation is tapped with the simple intent. I am lost for words as to how strange this bug is, I've never ever seen this in my life...

CodePudding user response:

There is something going on with your code that is touching your views that is causing the multiple layouts. Without that code being posted, it is impossible to say what.

To help you with debugging, set the one shot to the following:

OneShotPreDrawListener.add(binding.root) {
    Log.d("Applog", "OneShot")
    binding.activityCanvasTransparentBackgroundView.layoutParams.width =
        binding.activityCanvasDistanceContainer.measuredWidth
    binding.activityCanvasTransparentBackgroundView.layoutParams.height =
        binding.activityCanvasDistanceContainer.measuredHeight
    binding.activityCanvasTransparentBackgroundView.requestLayout()
}

In my test, when placing this in the onCreateView() function of a fragment, "OneShot" is logged just once and the size of the view is adjusted as you say it should be. (btw, requestLayout() was required in this test.)

I would take a look at any other code that touches your views to see if something else is triggering the multiple calls: maybe setWidth() or setHeight().



I will add that I have noticed in the past that ConstraintLayout will call the global layout listener multiple times. Usually, I would say, the listener is removed on the first call, so this behavior would not be noticed.

Instead of the global layout listener or the predraw listener, you could try a layout listener:

binding.root.doOnLayout {
    // Your code here
}

or

binding.root.doOnNextLayout {
    // Your code here
}

doOnNextLayout is guaranteed to be called only once, while doOnLayout may be called multiple times.

CodePudding user response:

Wow guys.

I finally found a solution, after 12 hours of nonstop debugging, almost going clinically insane, and logging and destroying my codebase.

I realized that the keyboard from the NewProjectFragment was cutting the view, causing the bug. Holy moly... I am ecstatic that I finally found why this is happening but at the same time shocked how I didn't discover this before!

The solution was to add the following to the activity:

     <activity
            android:name="com.therealbluepandabear.pixapencil.activities.canvas.CanvasActivity"
            android:windowSoftInputMode="stateHidden" // this
            android:hardwareAccelerated="true" 
            android:exported="false" />

Prior to this, the windowSoftInputMode was adjustResize -- causing the bug.

  • Related