Disable focus on fragment
Asked Answered
S

4

10

I'm working on application for TV platform and use RCU for navigation.

I have use case where I have two fragments one above each other and visible on the screen at the same time.

Is there a way to disable focusing fragment that is below? setFocusable(false) on fragments view doesnt work, and I'm able to focus elements in fragment below.

Thanks in advance.

Swahili answered 4/9, 2017 at 9:45 Comment(5)
you can add setonclicklistner in onCreate programmatically.Meddle
something like this. https://mcmap.net/q/1166651/-how-to-remove-the-focus-of-the-back-stack-fragmentMeddle
Why onClickListener? I need something like onFocusChanged? I dont use touch events, it is Android TV with remote controller.Swahili
@Swahili Can you find a solution? I have the same problemEngine
@Engine I have posted solution that I've ended up using.Swahili
S
5

The solution that I've come up with in the end is:

Added custom Lifecycle listeners for the fragment namely: onFragmentResume and onFragmentPause events which I call manually when I need to show/hide or switch between fragments.

@Override
public void onFragmentResume() {

    //Enable focus
    if (getView() != null) {

        //Enable focus
        setEnableView((ViewGroup) view, true);

        //Clear focusable elements
        focusableViews.clear();
    }

    //Restore previous focus
    if (previousFocus != null) {
        previousFocus.requestFocus();
    }
}

@Override
public void onFragmentPause() {

    //Disable focus and store previously focused
    if (getView() != null) {

        //Store last focused element
        previousFocus = getView().findFocus();

        //Clear current focus
        getView().clearFocus();

        //Disable focus
        setEnableView((ViewGroup) view, false);
    }
}

/**
 * Find focusable elements in view hierarchy
 *
 * @param viewGroup view
 */
private void findFocusableViews(ViewGroup viewGroup) {

    int childCount = viewGroup.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View view = viewGroup.getChildAt(i);
        if (view.isFocusable()) {
            if (!focusableViews.contains(view)) {
                focusableViews.add(view);
            }
        }
        if (view instanceof ViewGroup) {
            findFocusableViews((ViewGroup) view);
        }
    }
}

/**
 * Enable view
 *
 * @param viewGroup
 * @param isEnabled
 */
private void setEnableView(ViewGroup viewGroup, boolean isEnabled) {

    //Find focusable elements
    findFocusableViews(viewGroup);

    for (View view : focusableViews) {
        view.setEnabled(isEnabled);
        view.setFocusable(isEnabled);
    }
}
Swahili answered 7/2, 2018 at 13:23 Comment(0)
C
2

I had the same problem and the accepted answer worked for me.

Here is my version of the implementation so far (it can be improved):

abstract class BaseFragment<....> : Fragment() {

    private val screenFocusHelper = ScreenFocusHelper()

    fun enableFocus() {
        if (view != null) {
            // Enable focus
            screenFocusHelper.setEnableView(view as ViewGroup, true)

            // Clear focusable elements
            screenFocusHelper.focusableViews.clear()
        }

        childFragmentManager.fragments.forEach {
            if (it is BaseFragment<*, *>) {
                it.enableFocus()
            }
        }
    }

    fun disableFocus() {
        if (view != null) {
            // Store last focused element
            screenFocusHelper.previousFocus = view?.findFocus()

            // Clear current focus
            view!!.clearFocus()

            // Disable focus
            screenFocusHelper.setEnableView(view as ViewGroup, false)
        }

        childFragmentManager.fragments.forEach {
            if (it is BaseFragment<*, *>) {
                it.disableFocus()
            }
        }
    }

}

class ScreenFocusHelper {

    var previousFocus: View? = null

    val focusableViews: MutableList<View> = mutableListOf()

    fun setEnableView(viewGroup: ViewGroup, isEnabled: Boolean) {
        findFocusableViews(viewGroup)

        for (view in focusableViews) {
            view.isEnabled = isEnabled
            view.isFocusable = isEnabled
        }
    }

    private fun findFocusableViews(viewGroup: ViewGroup) {
        val childCount = viewGroup.childCount
        for (i in 0 until childCount) {
            val view = viewGroup.getChildAt(i)
            if (view.isFocusable) {
                if (!focusableViews.contains(view)) {
                    focusableViews += view
                }
            }
            if (view is ViewGroup) {
                findFocusableViews(view)
            }
        }
    }

}
Chloral answered 8/11, 2018 at 14:23 Comment(0)
M
1

This is my solution based on the GuidedStepFragment from leanback, that will block any focus on the view that is not a child of your layout.

Create a custom view and make it as a root layout for the second Fragment that is on the top:

import android.R.attr
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout

class GuidedConstraintLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

private var mFocusOutStart = false
private var mFocusOutEnd = false

fun setFocusOutStart(focusOutStart: Boolean) {
    mFocusOutStart = focusOutStart
}

fun setFocusOutEnd(focusOutEnd: Boolean) {
    mFocusOutEnd = focusOutEnd
}

override fun focusSearch(focused: View?, direction: Int): View? {
    val newFocus = super.focusSearch(focused, direction)
    if (direction == FOCUS_LEFT || direction == FOCUS_RIGHT || direction == FOCUS_UP || direction == FOCUS_DOWN) {
        if (isDescendant(this, newFocus)) {
            return newFocus
        }
        if (if (layoutDirection == LAYOUT_DIRECTION_LTR) attr.direction == FOCUS_LEFT else attr.direction == FOCUS_RIGHT) {
            if (!mFocusOutStart) {
                return focused
            }
        } else {
            if (!mFocusOutEnd) {
                return focused
            }
        }
    }
    return newFocus
}

private fun isDescendant(parent: ViewGroup, child: View?): Boolean {
    var localChild = child
    while (localChild != null) {
        if (localChild === parent) {
            return true
        }
        val p = localChild.parent
        if (p !is View) {
            return false
        }
        localChild = p
    }
    return false
}
}
Manolo answered 27/6, 2022 at 9:0 Comment(0)
G
0

I solved this making a custom class for the parent ViewGroup of the fragment layout. In your custom ViewGroup you can override the method

focusSearch(focused: View?, direction: Int)

Adding this logic:

override fun focusSearch(focused: View?, direction: Int): View? {

        val focusSearch = super.focusSearch(focused, direction)

        if (direction == View.FOCUS_DOWN) when (focused?.id) {
            R.id.some_view_that_has_focus -> return new_view_focus
        }

        if (findViewById<View>(focusSearch.id) == null) {//the view found is not part of this parent return null
            return null
        }

        return focusSearch
}

When the new focused view is not part of this parent than return null. When some other Views focus is not reachable I manage it manually inside when cases.

Gusgusba answered 30/10, 2018 at 17:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.