Обробка кнопки повернення в компоненті навігації Android


83

Я хотів би знати, як правильно обробляти дії кнопки повернення системи за допомогою навігаційного контролера. У моєму додатку у мене є два фрагменти (наприклад, фрагмент1 та фрагмент2), і я маю дію у фрагменті1 з призначенням до фрагмента2. Все працює добре, крім одного - коли користувач натискає кнопку повернення системи у фрагменті2, я хочу показати діалогове вікно (наприклад, використовуючи DialogFragment) для підтвердження виходу. Який найкращий спосіб реалізувати таку поведінку? Якщо я використовую app:defaultNavHost="true"у своєму фрагменті хосту, він автоматично повертається назад, ігноруючи мої правила. І, крім того, для чого цей компонент?

введіть тут опис зображення

Чи варто використовувати "pop to"?


1
За допомогою "Поп до" ви можете визначити, куди йти (пункт призначення), натискаючи кнопку резервного копіювання.
— Алекс

@Alex Отже, якщо для нього встановлено значення none, як він повинен реагувати на кнопку повернення?
— Кирил Ткач,

1
Коли для нього встановлено значення "немає", поведінка за замовчуванням, користувач буде переведений до попереднього пункту призначення (фрагмент 1)
— Олексій

@Alex, гаразд, чи є спосіб обробити кнопку назад за другим фрагментом?
— Кирил Ткач,

Відповіді:


146

Останнє оновлення - 25 квітня 2019 р

Новий випуск androidx.activity ver. 1.0.0-alpha07 вносить деякі зміни

Більше пояснень в офіційному посібнику для Android: Забезпечте користувальницьку зворотну навігацію

Приклад:

public class MyFragment extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // This callback will only be called when MyFragment is at least Started.
        OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
            @Override
            public void handleOnBackPressed() {
                // Handle the back button event
            }
        };
        requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);

        // The callback can be enabled or disabled here or in handleOnBackPressed()
    }
    ...
}

Старі оновлення

UPD: 3 квітня 2019 р

Тепер це спрощено. Більше інформації тут

Приклад:

requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), this);

@Override
public boolean handleOnBackPressed() {
    //Do your job here
    //use next line if you just need navigate up
    //NavHostFragment.findNavController(this).navigateUp(); 
    //Log.e(getClass().getSimpleName(), "handleOnBackPressed");
    return true;
    }

Не підтримується (з версії 1.0.0-alpha06, 3 квітня 2019 р.):

Так як це , вона може бути реалізована тільки з допомогою JetPack реалізації OnBackPressedCallbackв вашому фрагменті і додати його до діяльності: getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);

Ваш фрагмент повинен виглядати так:

public MyFragment extends Fragment implements OnBackPressedCallback {

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);
}
    
    @Override
    public boolean handleOnBackPressed() {
        //Do your job here
        //use next line if you just need navigate up
        //NavHostFragment.findNavController(this).navigateUp(); 
        //Log.e(getClass().getSimpleName(), "handleOnBackPressed");
        return true;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        getActivity().removeOnBackPressedCallback(this);
    }
}

UPD: Ваша активність повинна розширюватися AppCompatActivityабо FragmentActivityу файлі Gradle:

 implementation 'androidx.appcompat:appcompat:{lastVersion}'

4
Чи існує це ще в Jetpack?
— Bryan Bryce

1
Якщо ваша активність AppCompat не з підтримки lib, а з androidX lib, тоді так, ви повинні
— Jurij Pitulja

3
Використовуючи AndroidX AppCompatActivity та Androidx Fragment, і я теж не бачу цього як доступний варіант
— Omega142

3
OnBackPressCallback - це абстрактний клас зараз! Тож це рішення для мене не працює.
— Джунія Монтана

3
Як зробити звичайну операцію задньої панелі після виконання деякої логіки на - handleIOnBackPress ()?
— krishnakumarcn

22

Отже, я створив інтерфейс

public interface OnBackPressedListener {
    void onBackPressed();
}

І реалізували це усі фрагменти, які повинні обробляти кнопку повернення. В основній діяльності я замінив onBackPressed()метод:

@Override
public void onBackPressed() {
    final Fragment currentFragment = mNavHostFragment.getChildFragmentManager().getFragments().get(0);
    final NavController controller = Navigation.findNavController(this, R.id.nav_host_fragment);
    if (currentFragment instanceof OnBackPressedListener)
        ((OnBackPressedListener) currentFragment).onBackPressed();
    else if (!controller.popBackStack())
        finish();

}

Отже, якщо верхній фрагмент мого хосту навігації реалізує OnBackPressedListenerінтерфейс, я викликаю його onBackPressed()метод, в іншому випадку я просто вивожу назад стек і закриваю програму, якщо задній стек порожній.


22
Гідне рішення. Хоча мені завжди цікаво, в чому сенс Навігаційного компонента, якщо йому не вистачає такої функціональності нестандартно.
— First_Strike

Перевірте відповідь @Jurij Pitulja - це рекомендований спосіб вирішити проблему
— Мітч,

Навіщо нам потрібен окремий інтерфейс замість того, щоб використовувати те, що є в Jetpack?
— ІгорГанапольський

@IgorGanapolsky на момент цієї відповіді в Jetpack не було інтерфейсу.
— Кирил Ткач,

@KirylTkach що таке декларація mNavHostFragment?
— E.Akio

16

Рекомендуються підхід полягає в додаванні OnBackPressedCallbackдо даного виду діяльності OnBackPressedDispatcher.

requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { 
    // handle back event
}

1
краще, якщо власник зворотного виклику також передається таким чином .addCallback(viewLifecycleOwner) {}, інакше ви продовжуватимете отримувати зворотні дзвінки навіть після того, як фрагмент буде знищений.
— Марат

13

Для тих, хто шукає реалізацію Kotlin, дивіться нижче.

Зверніть увагу, що OnBackPressedCallback єдиний, здається, працює для забезпечення користувацької поведінки назад для вбудованої програмної / апаратної кнопки повернення, а не кнопки зі стрілкою назад / додому як кнопки вгору на панелі дій / панелі інструментів. Щоб також замінити поведінку кнопки назад на панелі дій / панелі інструментів, я надаю рішення, яке працює для мене. Якщо це помилка або ви знаєте найкраще рішення для цього випадку, будь ласка, коментуйте.

build.gradle

...
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.navigation:navigation-fragment-ktx:2.0.0"
implementation "androidx.navigation:navigation-ui-ktx:2.0.0"
...

MainActivity.kt

...
import androidx.appcompat.app.AppCompatActivity
...

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

        ...

        val navController = findNavController(R.id.nav_host_fragment)
        val appBarConfiguration = AppBarConfiguration(navController.graph)

        // This line is only necessary if using the default action bar.
        setupActionBarWithNavController(navController, appBarConfiguration)

        // This remaining block is only necessary if using a Toolbar from your layout.
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        toolbar.setupWithNavController(navController, appBarConfiguration)
        // This will handle back actions initiated by the the back arrow 
        // at the start of the toolbar.
        toolbar.setNavigationOnClickListener {
            // Handle the back button event and return to override 
            // the default behavior the same way as the OnBackPressedCallback.
            // TODO(reason: handle custom back behavior here if desired.)

            // If no custom behavior was handled perform the default action.
            navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
        }
    }

    /**
     * If using the default action bar this must be overridden.
     * This will handle back actions initiated by the the back arrow 
     * at the start of the action bar.
     */
    override fun onSupportNavigateUp(): Boolean {
        // Handle the back button event and return true to override 
        // the default behavior the same way as the OnBackPressedCallback.
        // TODO(reason: handle custom back behavior here if desired.)

        // If no custom behavior was handled perform the default action.
        val navController = findNavController(R.id.nav_host_fragment)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}

MyFragment.kt

...
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
...

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val onBackPressedCallback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // Handle the back button event
            }
        }
        requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback)
    }
}

Офіційну документацію можна переглянути за адресою https://developer.android.com/guide/navigation/navigation-custom-back


1
Це найкраща відповідь. Ви повинні мати можливість обробляти зворотне натискання з панелі інструментів та звичайний зворотний тиск. Великі пальці вгору
— odifek

Це мені дуже допомогло! Кнопка "Назад" на панелі інструментів не спрацьовувала, OnBackPressedCallbackале після додавання toolbar.setNavigationOnClickListener { onBackPressed() }вона спрацювала так, що тепер апаратне забезпечення назад і панель інструментів назад працюють однаково. Дякуємо за чітку детальну відповідь!
— poby

Це правильне і найбільш логічне рішення. Напевно витримає перевірку часом.
— Роуленд Мтетезі,

Я використовував лише код для перезаписування фрагмента, він працює чудово!
— sud007

10

Ось рішення, яке має робити те, що ви хочете, але я вважаю це поганим рішенням, оскільки воно суперечить ідеї компонента навігації Android (дозволяючи андроїду обробляти навігацію).

Перевизначте "onBackPress" усередині своєї діяльності

override fun onBackPressed() {
    when(NavHostFragment.findNavController(nav_host_fragment).currentDestination.id) {
        R.id.fragment2-> {
            val dialog=AlertDialog.Builder(this).setMessage("Hello").setPositiveButton("Ok", DialogInterface.OnClickListener { dialogInterface, i ->
                finish()
            }).show()
        }
        else -> {
            super.onBackPressed()
        }
    }
} 

1
Дякую за ваше рішення, але я використав це, перш ніж вирішив перейти на компонент Навігація. Здається, це занадто рано :(
— Кирил Ткач,

ще рано. я застосував аналогічне хакейне рішення для обробки навігації на панелі
— додатків

9

Я писав в основній діяльності, як це,

override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.my_nav_host_fragment).navigateUp(appBarConfiguration)
    }   

6

В 2.1.0-alpha06

Якщо ви хочете обробляти зворотний тиск лише у поточному фрагменті

requireActivity().onBackPressedDispatcher.addCallback(this@LoginFragment) {
    // handle back event
}

На всю діяльність

requireActivity().onBackPressedDispatcher.addCallback() {
    // handle back event
}

6

Це 2 рядки коду, які можна прослуховувати для зворотного друку, з фрагментів, [ТЕСТОВАНО та РОБОЧЕ]

  requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {

            //setEnabled(false); // call this to disable listener
            //remove(); // call to remove listener
            //Toast.makeText(getContext(), "Listing for back press from this fragment", Toast.LENGTH_SHORT).show();
     }

5

Трохи пізно на вечірку, але з останнім випуском Navigation Component 1.0.0-alpha09, тепер у нас є AppBarConfiguration.OnNavigateUpListener.

Для отримання додаткової інформації зверніться до цих посилань: https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener https://developer.android.com/jetpack/docs/release-notes


Дякуємо, що показали мені примітки до випуску! З’ясувалося, що android: menuCategory = "secondary" уникає появи заднього стека!
— Валентин Гавран

для мене це працює лише з панеллю інструментів, але не з кнопкою "Назад"
— Юрій Пітуля

3

Рекомендований метод працював у мене, але після оновлення бібліотеки implementation 'androidx.appcompat:appcompat:1.1.0'

Реалізуйте, як показано нижче

 val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Handle the back button event
        }
    }
    requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)

за допомогою Котліна


2

Ви можете забезпечити власну зворотну навігацію за допомогою OnBackPressDispatcher

class MyFragment : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // This callback will only be called when MyFragment is at least Started.
    val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
        // Handle the back button event
// and if you want to need navigate up
//NavHostFragment.findNavController(this).navigateUp()
    }

    // The callback can be enabled or disabled here or in the lambda
}
}

Більше пояснень в офіційному посібнику для Android: https://developer.android.com/guide/navigation/navigation-custom-back


1

Якщо ви використовуєте компонент навігації, дотримуйтесь наведених нижче кодів у методі onCreateView () (у цьому прикладі я хочу просто закрити свою програму цим фрагментом)

 OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {
            new AlertDialog.Builder(Objects.requireNonNull(getActivity()))
                    .setIcon(R.drawable.icon_01)
                    .setTitle(getResources().getString(R.string.close_app_title))
                    .setMessage(getResources().getString(R.string.close_app_message))
                    .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            getActivity().finish();
                        }
                    })
                    .setNegativeButton(R.string.no, null)
                    .show();
        }
    };
    requireActivity().getOnBackPressedDispatcher().addCallback(this, backPressedCallback);

1

І якщо ви хочете мати таку ж поведінку і для кнопки повернення панелі інструментів, просто додайте це у свою активність:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == android.R.id.home) {
        getOnBackPressedDispatcher().onBackPressed();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

1

Використовуйте це, якщо ви використовуєте фрагмент, або додайте його в прослуховувач натискання кнопки. Це працює для мене.

requireActivity().onBackPressed()

Викликається, коли діяльність виявляє натискання користувачем клавіші "Назад". GetOnBackPressDispatcher () OnBackPressDispatcher} отримає шанс обробити кнопку "Назад" до того, як буде викликано поведінку android.app.Activity # onBackPress ()} за замовчуванням.


1

Просто додайте ці рядки

     override fun onBackPressed() {
            if(navController.popBackStack().not()) {
            //Last fragment: Do your operation here 
            finish()
   } 

navController.popBackStack () просто виведе ваш фрагмент, якщо це не ваш останній фрагмент


0

Ось моє рішення

Використовуйте androidx.appcompat.app.AppCompatActivityдля діяльності, яка містить NavHostFragmentфрагмент.

Визначте наступний інтерфейс та впровадіть його у всі фрагменти призначення навігації

interface InterceptionInterface {

    fun onNavigationUp(): Boolean
    fun onBackPressed(): Boolean
}

У своїй діяльності перевизначте onSupportNavigateUpта onBackPressed:

override fun onSupportNavigateUp(): Boolean {
        return getCurrentNavDest().onNavigationUp() || navigation_host_fragment.findNavController().navigateUp()
}

override fun onBackPressed() {
        if (!getCurrentNavDest().onBackPressed()){
            super.onBackPressed()
        }
}

private fun getCurrentNavDest(): InterceptionInterface {
        val currentFragment = navigation_host_fragment.childFragmentManager.primaryNavigationFragment as InterceptionInterface
        return currentFragment
}

Це рішення має ту перевагу, що фрагментам пунктів навігації не потрібно турбуватися про скасування реєстрації своїх слухачів, як тільки вони від'єднаються.


0

Я спробував рішення Юрія Пітулі, але мені просто не вдалося знайти getOnBackPressDispatcher або addOnBackPressCallback, також використовуючи рішення Кірила Ткача, не вдалося знайти поточний фрагмент, тому ось мій:

interface OnBackPressedListener {
    fun onBackPressed(): Boolean
}

override fun onBackPressed() {
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
    val currentFragment = navHostFragment?.childFragmentManager!!.fragments[0]
    if (currentFragment !is OnBackPressedListener || !(currentFragment as OnBackPressedListener).onBackPressed()) super.onBackPressed()

таким чином, ви можете фрагментарно вирішити, чи повинна дія взяти під контроль тиск назад чи ні.

Крім того, у вас є BaseActivity для всіх ваших дій, ви можете реалізувати так

override fun onBackPressed() {
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
    if (navHostFragment != null){
        val currentFragment = navHostFragment.childFragmentManager.fragments[0]
        if (currentFragment !is AuthContract.OnBackPressedListener ||
                !(currentFragment as AuthContract.OnBackPressedListener).onBackPressed()) super.onBackPressed()
    } else {
        super.onBackPressed()
    }
}

0

Якщо ви використовуєте BaseFragment для своєї програми, тоді ви можете додати onBackPressDispatcher до свого базового фрагмента.

//Make a BaseFragment for all your fragments
abstract class BaseFragment : Fragment() {

private lateinit var callback: OnBackPressedCallback

/**
 * SetBackButtonDispatcher in OnCreate
 */

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setBackButtonDispatcher()
}

/**
 * Adding BackButtonDispatcher callback to activity
 */
private fun setBackButtonDispatcher() {
    callback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            onBackPressed()
        }
    }
    requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}

/**
 * Override this method into your fragment to handleBackButton
 */
  open fun onBackPressed() {
  }

}

Перевизначте onBackPress () у своєму фрагменті, розширивши базовий фрагмент

//How to use this into your fragment
class MyFragment() : BaseFragment(){

private lateinit var mView: View

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    mView = inflater.inflate(R.layout.fragment_my, container, false)
    return mView.rootView
}

override fun onBackPressed() {
    //Write your code here on back pressed.
}

}


0

Залежно від вашої логіки, якщо ви хочете закрити лише поточний фрагмент, вам потрібно передати viewLifecycleOwner, код показано нижче:

   requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            requireActivity().finish()
        }
    })

Однак, якщо ви хочете закрити програму на backPress, незалежно від того, з якого фрагмента (можливо, ви цього не хотіли б!), Не передавайте viewLifecycleOwner. Також якщо ви хочете відключити кнопку "Назад", нічого не робіть всередині handleOnBackPress (), див. Нижче:

 requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // do nothing it will disable the back button
        }
    })

0

Спробуйте це. Думаю, це вам допоможе.

override fun onBackPressed() {
    when (mNavController.getCurrentDestination()!!.getId()) {

        R.id.loginFragment -> {
            onWarningAlertDialog(this, "Alert", "Do you want to close this application ?")
        }
        R.id.registerFragment -> {
            super.onBackPressed()
        }
    }
}



private fun onWarningAlertDialog(mainActivity: MainActivity, s: String, s1: String) {

        val dialogBuilder = AlertDialog.Builder(this)
        dialogBuilder.setMessage(/*""*/s1)
                .setCancelable(false)
                .setPositiveButton("Proceed", DialogInterface.OnClickListener { dialog, id ->
                    finish()
                })
                .setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, id ->
                    dialog.cancel()
                })

        // create dialog box
        val alert = dialogBuilder.create()
        // set title for alert dialog box
        alert.setTitle("AlertDialogExample")
        // show alert dialog
        alert.show()
    }

0

Я шукав багато тем, і жоден з них не працює. Нарешті я знайшов:

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toolbar mToolbar = findViewById(R.id.topAppBar);
    setSupportActionBar(mToolbar);
}

@Override
public boolean onSupportNavigateUp() {
    navController.navigateUp();
    return super.onSupportNavigateUp();
}

MyFragment.java

@Override
public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
    Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
    mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Do something when uses presses back button (showing modals, messages,...)
            // Note that this will override behaviour of back button
        }
    });
}

@Override
public void onStop() {
    // Reset back button to default behaviour when we leave this fragment
    Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
    mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mainActivity.onBackPressed();
        }
    });

    super.onStop();
}

0

просто створіть функцію розширення для фрагмента

fun Fragment.onBackPressedAction(action: () -> Boolean) {
    requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
        OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            this.isEnabled = action()
            if (!this.isEnabled) {
                requireActivity().onBackPressed()
            }
        }
    })
}

а після у фрагменті помістіть код у onCreateView (дія повинна повернути false, щоб викликати активність onBackPress)

onBackPressedAction { //do something }

0

Мені потрібно підтримувати як справжню кнопку "Назад", так і кнопку "Назад" на панелі інструментів з можливістю перевизначення клацання "Назад" в обох випадках (щоб показати діалогове вікно або щось інше). Я зробив додатковий метод в активності та відповідні логічні перевірки ('onBackPress' у моєму випадку) у фрагментах:

// Process hardware Back button
override fun onBackPressed() {
    if (canCloseActivity()) {
        super.onBackPressed()
    }
}

// Process toobar Back and Menu button
override fun onSupportNavigateUp(): Boolean {
    if (canCloseActivity()) {
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
    return false
}

// Do real check if has unfinished tasks, return false to override activity closing
private fun canCloseActivity(): Boolean {
    val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment

    return when {
        currentFragment is MyFragment && currentFragment.onBackPressed() -> false

        drawerLayout.isOpen -> {
            drawerLayout.close()
            false
        }
        fullScreenPreviewLayout.visibility == View.VISIBLE -> {
            closeFullscreenPreview()
            false
        }
        else -> true
    }
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.