簡體   English   中英

處理 Android 導航組件中的后退按鈕

[英]Handling back button in Android Navigation Component

我想知道如何使用導航 Controller 正確處理系統后退按鈕操作。在我的應用程序中,我有兩個片段(例如片段 1 和片段 2),並且我在片段 1 中有一個動作,目標是片段 2。 除了一件事,一切都很好——當用戶在 fragment2 中按下系統后退按鈕時,我想顯示一個對話框(例如使用 DialogFragment)來確認退出。 實現此行為的最佳方法是什么? 如果我在我的主機片段中使用app:defaultNavHost="true"那么它會自動返回並忽略我的規則。 另外,這個組件是做什么用的?

在此處輸入圖像描述

我應該使用“pop to”嗎?

最新更新 - 2019 年 4 月 25 日

新版本androidx.activity 版本。 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:2019 年 4 月 3 日

現在它簡化了。 更多信息在這里

例子:

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;
    }

已棄用(自 2019 年 4 月 3 日版本 1.0.0-alpha06 起):

既然這樣,就可以在片段中使用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:您的活動應擴展AppCompatActivityFragmentActivity並在 Gradle 文件中:

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

所以,我創建了一個界面

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()方法,在其他地方我只是簡單地彈出堆棧並在返回堆棧為空時關閉應用程序。

對於任何尋找 Kotlin 實現的人,請參見下文。

請注意, OnBackPressedCallback似乎僅適用於為內置軟件/硬件后退按鈕提供自定義后退行為,而不是將后退箭頭按鈕/主頁作為操作欄/工具欄中的向上按鈕。 為了還覆蓋操作欄/工具欄后退按鈕的行為,我提供了適合我的解決方案。 如果這是一個錯誤,或者您知道針對這種情況的更好解決方案,請發表評論。

構建.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()
    }
}

我的片段.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查看

推薦的方法是將OnBackPressedCallback添加到活動的OnBackPressedDispatcher中。

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

這是應該做你想做的解決方案,但我認為這是一個糟糕的解決方案,因為它違背了 Android 導航組件的想法(讓 android 處理導航)。

在您的活動中覆蓋“onBackPressed”

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()
        }
    }
} 

我在這樣的主要活動中寫道,

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

21 年 4 月 22 日更新

我正在更新我的答案以展示推薦方法的示例,這也是上面接受的答案

class MyFragment : Fragment() {

    ...
    
    private val backPressedDispatcher = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Redirect to our own function
            this@MyFragment.onBackPressed()
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        setHasOptionsMenu(true) //Set this to true in order to trigger callbacks to Fragment#onOptionsItemSelected

        (requireActivity() as AppCompatActivity).apply {
            // Redirect system "Back" press to our dispatcher
            onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedDispatcher)

            // Set toolbar if it is in Fragment's layout. If you have a global toolbar that lives in Activity layout, then you don't need this line.
            setSupportActionBar(view.findViewById(R.id.toolbar))

            // Setup action bar to work with NavController
            setupActionBarWithNavController(findNavController())
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return if (item.itemId == android.R.id.home) {
            // Redirect "Up/Home" button clicks to our own function
            this@MyFragment.onBackPressed()
            true
        } else {
            super.onOptionsItemSelected(item)
        }
    }

    private fun onBackPressed() {
        // Work your magic! Show dialog etc.
    }

    override fun onDestroyView() {
        // It is optional to remove since our dispatcher is lifecycle-aware. But it wouldn't hurt to just remove it to be on the safe side.
        backPressedDispatcher.remove() 
        super.onDestroyView()
    }

 }

原始答案2019 年 1 月 3 日

聚會有點晚了,但是隨着導航組件 1.0.0-alpha09 的最新版本,現在我們有了一個 AppBarConfiguration.OnNavigateUpListener。

有關更多信息,請參閱這些鏈接: https ://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener https://developer.android.com/jetpack/docs/release-notes

2.1.0-alpha06

如果您只想在當前片段中處理回壓

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

對於整個活動

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

這是 2 行代碼,可以從片段[TESTED and WORKING]中監聽回壓

  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();
     }

只需添加這些行

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

如果這不是您的最后一個片段, navController.popBackStack()只會彈出您的片段

推薦的方法對我有用,但在更新我的庫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)

使用 Kotlin

您可以使用 OnBackPressedDispatcher 提供自定義后退導航

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

如果您使用片段或將其添加到按鈕單擊偵聽器中,請使用此選項。 這對我有用。

requireActivity().onBackPressed()

當活動檢測到用戶按下返回鍵時調用。 getOnBackPressedDispatcher() OnBackPressedDispatcher} 將有機會在調用 android.app.Activity#onBackPressed()} 的默認行為之前處理后退按鈕。

使用導航組件這對我有好處:

Navigation.findNavController(requireView()).popBackStack()

安卓文檔

片段擴展.kt

fun Fragment.onBackPressedCustomAction(action: () -> Unit) {
  requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
    override
    fun handleOnBackPressed() {
      action()
    }
  })
}

YourPrettyFragment.kt

onBackPressedCustomAction {
  // Your custom action here
}

如果您使用導航組件,請在 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);

只需為片段創建一個擴展函數

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 才能調用活動 onBackPressed)

onBackPressedAction { //do something }

嘗試這個。 我想這會對你有所幫助。

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()
    }

如果您為您的應用程序使用 BaseFragment,那么您可以將 onBackPressedDispatcher 添加到您的基本片段中。

//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() {
  }

}

通過擴展 basefragment 覆蓋片段中的 onBackPressed()

//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.
}

}

如果您希望工具欄后退按鈕也具有相同的行為,只需在您的活動中添加:

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

如果您實際上是在嘗試專門處理后退按鈕,那么您可以使用@Jurij Pitulja 回答。

但是,如果你想要 pop SecondFragment (開始片段FirstFragment )而不返回FirstFragment ,那么你可以使用:

Navigation.findNavController(view).popBackStack()

SecondFragment 這樣,當您從FirstFragment按下返回按鈕時,您將彈出返回堆棧的SecondFragmetn ,並且不會返回到SecondFragment

科特林答案

使用popBackStack()示例:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    mButton.setOnClickListener {
        Navigation.findNavController(view).popBackStack() // You need this line.
    }
}

這是我的解決方案

androidx.appcompat.app.AppCompatActivity用於包含NavHostFragment片段的活動。

定義如下接口並在所有導航目的地片段中實現

interface InterceptionInterface {

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

在您的活動中覆蓋onSupportNavigateUponBackPressed

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
}

這種解決方案的優點是導航目的地片段在分離后無需擔心其偵聽器的注銷。

我嘗試了 Jurij Pitulja 解決方案,但我無法找到 getOnBackPressedDispatcher 或 addOnBackPressedCallback 也使用 Kiryl Tkach 的解決方案無法找到當前片段,所以這是我的:

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()
    }
}

根據您的邏輯,如果您只想關閉必須通過 viewLifecycleOwner 的當前片段,代碼如下所示:

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

但是,如果您想在 backPressed 上關閉應用程序,無論從哪個片段(可能您不希望這樣!),都不要傳遞 viewLifecycleOwner。 此外,如果您想禁用后退按鈕,請不要在 handleOnBackPressed() 中執行任何操作,請參見下文:

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

我的意見 requireActivity().onBackPressed()

requireActivity().onBackPressed()

我搜索了很多線程,但沒有一個有效。 最后我找到了一個:

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();
}

我需要同時支持真正的后退按鈕和工具欄后退按鈕,並且能夠在兩種情況下覆蓋“后退”單擊(以顯示對話框或其他內容)。 我在片段中創建了一個額外的方法和相應的布爾檢查(在我的例子中是'onBackPressed'):

// 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
    }
}

簡單地說,在您的FragmentonCreate()方法中,在super.onCreate(savedInstanceState)之后使用此代碼:

// This callback will only be called when MyFragment is at least Started.
val callback = requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
            // Handle the back button event
}

它的ktx版本:

fun Fragment.handleBackButtonEvent(
    onBackPressed: OnBackPressedCallback.() -> Unit
) {
    requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
        onBackPressed()
    }
}

您可以簡單地在Fragmnet中使用它。

如果您使用的是 NavController,這是一個答案:

Navigation.findNavController(view).navigateUp();

不要嘗試破解其他方法,例如在事務中使用 FragmentManager 和 replace() 替換片段。 NavController 負責這個。

問題

提供自定義后退導航的官方方法僅適用於本機按鈕。 此方法可以(通常必須)在具有不同 viewModel 的其他甚至多個片段中聲明,以決定導航應該發生什么(簡單示例:如果 x 則顯示保存對話框,否則正常導航回來)。

解決方案

  • onBackPressed模擬任何片段中的本機后退。

  • setNavigationOnClickListener可以設置為監聽來自非原生后退按鈕的事件。

執行

MainActivity.kt中的setupWithNavController之后,添加:

myToolbar.setNavigationOnClickListener{
    onBackPressed()
}

另請參閱 Aptin Grabian 的回答和 poby 的評論。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM