TakuSemba / Spotlight

Android Library that lights items for tutorials or walk-throughs etc...
Apache License 2.0
3.63k stars 364 forks source link

Add responsive layout #116

Open jlucas577 opened 3 years ago

jlucas577 commented 3 years ago

Hello guys! I opened this pr to get help on a possible responsiveness of the library, my problem is that when I position the view on a particular float, on larger screens it is excellent and working very well, however on smaller screens it just stops being "responsive" and the view ends up being on top of others that it shouldn't ..

I've tried to set the screen percentage as a top margin for example, but the problem still persists, and the funniest thing is that it only happens on screens smaller than 5 inches.

Prinscreen Prinscreen

Albul commented 3 years ago

@jlucas577 First of all you need to fix your layout on a small screens. Second you need to redesign overlay view, and put title and description of your tutorial at the very top. Or when ui component is at the top of the screen you need to readjust title and description to the bottom. Check example from my app:

private fun initOverlayView(
        rootView: View,
        targetView: View,
        activity: RootActivity,
    ) {
       val overlay: View = activity.layoutInflater
            .inflate(R.layout.block_tutorial_feature, rootView, false)

        val targetWidth: Float = targetView.width.toFloat()
        val targetHeight: Float = targetView.height.toFloat()
        val targetMinSize: Float = min(targetWidth, targetHeight)
        val targetMaxSize: Float = max(targetWidth, targetHeight)
        val location = IntArray(2)
        targetView.getLocationInWindow(location)
        // This is coords on window. It is hard to say where is start point.
        // But it seems that in split screen mode without status bar,
        // but without split screen mode it includes status bar
        val targetLeft: Float = location[0].toFloat()
        val targetTop: Float = location[1].toFloat()
        val targetRight: Float = targetLeft + targetView.width
        val targetBottom: Float = targetTop + targetView.height
        val targetCenterX: Float = targetLeft + targetWidth * 0.5f
        val targetCenterY: Float = targetTop + targetHeight * 0.5f

        val horizontalPadding: Int = activity.calcTutTextHorizPadding(rootView)

        val isInMultiWindowMode: Boolean =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) activity.isInMultiWindowMode else false

        overlay.findViewById<View>(R.id.tutorial_container).let {
            it.setOnTouchListener { v, event ->
                val eventX = if (isInMultiWindowMode) event.x else event.rawX
                val eventY = if (isInMultiWindowMode) event.y else event.rawY

                if (eventX in targetLeft..targetRight
                    && eventY in targetTop..targetBottom
                    && isTargetClickable
                ) {
                    false // Pass event under the hole
                } else {
                    true // Consume event
                }
            }
        }

        val titleField = overlay.findViewById<TextView>(R.id.tutorial_title_field).also {
            it.updatePadding(left = horizontalPadding, right = horizontalPadding)

            if (title.isNullOrEmpty()) {
                it.visibility = View.GONE
            } else {
                it.text = title
            }
        }

        val descriptionField = overlay.findViewById<TextView>(R.id.tutorial_description_field).also {
            it.updatePadding(left = horizontalPadding, right = horizontalPadding)

            if (description.isNullOrEmpty()) {
                it.visibility = View.GONE
            } else {
                it.text = description
            }
        }

        val okButton = overlay.findViewById<MaterialButton>(R.id.tutorial_ok_button).also {
            if (hasOk) {
                it.setOnClickListener {
                    finish(activity)
                }
            } else {
                it.visibility = View.GONE
            }
        }

        overlay.findViewById<View>(R.id.tutorial_try_button).visibility = View.GONE

        overlay.findViewById<ViewGroup>(R.id.tutorial_content_container).let { container ->
            if (isContentTransparent) {
                container.setBackgroundColor(ColorsAcore.transparent)
            }

            titleField.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
            descriptionField.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
            iconView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)

            container.updateLayoutParams<RelativeLayout.LayoutParams> {
                // Status bar as margin between hole and content
                val contentMargin: Int = activity.getDimensionPixelSize(R.dimen.tutorial_content_vert_margin)
                // In multiwindow mode status bar not counted as window size
                val statusBarHeight: Int = if (isInMultiWindowMode) 0 else activity.statusBarHeight
                val contentHeight = titleField.measuredHeight + descriptionField.measuredHeight +
                    iconView.measuredHeight + contentMargin

                val windowHeight: Int = activity.windowHeightNoSysBars

                val topRestHeight: Float = targetTop - statusBarHeight
                val bottomRestHeight: Float = windowHeight - targetBottom

                when {
                    // If top and bottom rest smaller than content
                    topRestHeight < contentHeight
                        && bottomRestHeight < contentHeight -> {
                        if (!isContentTransparent) {
                            container.setBackgroundColor(ColorsApp.tutorialBgColor)
                        }
                        adjustOkButtonColor(okButton, targetBottom)
                    }

                    // Top rest much bigger than bottom one. Show content above hole
                    topRestHeight > bottomRestHeight * 1.3f -> {
                        this.bottomMargin = windowHeight - (targetTop.toInt() - statusBarHeight) + contentMargin
                        adjustOkButtonColor(okButton, targetBottom)
                    }

                    // Default, show content below the hole
                    else -> {
                        this.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE)
                        this.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                        // It is safe to set targetBottom as a topMargin, so there should be an extra margin
                        // between hole and top edge of the content which equals to notification bar height
                        this.topMargin = targetBottom.toInt() - statusBarHeight + contentMargin
                        this.bottomMargin = 0 // Or it will hide part of big content block
                    }
                }

            }
        }
    }
Albul commented 3 years ago

@jlucas577 Also when you have components inside ScrollView which you want to highlight. First you need to scroll ScrollView to ensure targetView is visible on a small screen.