精华内容
下载资源
问答
  • Android Navigation 是 Google Jetpack 里面的一个组件,支持 Android 应用里面的页面导航。我们开发的应用在 2.0 大版本的迭代的时候选择了这个技术...

    Android Navigation 是 Google Jetpack 里面的一个组件,支持 Android 应用里面的页面导航。

    我们开发的应用在 2.0 大版本的迭代的时候选择了这个技术方案,现在版本刚提测,趁着新鲜出炉,把遇到的坑和大家分享一下。

    先声明一下,这篇文章不是 Navigation 的入门文章和使用说明,也不是原理介绍,可以作为我们团队对 Navigation 使用的一些心得体会的说明,比较适合对 Navigation 有项目实践的同学。

    这里也欢迎有兴趣的同学,如果有这方面想交流的话,欢迎联系,一起探讨。以下都是一些从项目实践中的个人见解,免不了出纰漏,不吝斧正。

    Navigation 优点体会

    在使用过程中,我们感受到如下的优点。

    1. 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。

    2. 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。

    3. 统一的 Navigation API 来更精细的控制跳转逻辑。

    所有坑的中心

    Navigation 相关的坑,都有个中心。一般情况下,Fragment 就是一个 View,View 的生命周期就是 Fragment 的生命周期,但是在 Navigation 的架构下,Fragment 的生命周期和 View 的生命周期是不一样的。当 navigate 到新的 UI,被覆盖的 UI,View 被销毁,但是保留了 fragment 实例(未被 destroy),当这个 fragment 被 resume 的时候,View 会被重新创建。这是“罪恶”之源。

    整理了 8 个坑,我们一个一个趟

    先来个小坑感受一些 Navigation。

    1. Databinding 需要 onDestroyView 设置为 Null。

    现在大家都会使用 Jetpack 里面的 databinding 技术,这个确实可以帮助我们简化很多代码,其中的自感知的生命周期,可以帮我们只有在必要的时候来更新 UI。

    一般会在 Fragment 的 onCreateView 模板函数中初始化 ViewDataBing,这样就会有 Fragment 持有对 View 的引用。但是 fragment 和 view 的生命周期是不一样的,当 view 被销毁的时候,fragment 并不一定被销毁,所以一定要在 fragment.onDestroyView 函数中把对 view 的引用变量设置为 null,不然会导致 view 回收不掉。上一段官方的代码来说明一下。

    private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
    

    2. 当 Databinding 遇到错的 lifecycle.

    Databinding 确实很强大,能把数据和 UI 进行绑定,这里对 UI 就有个要求,UI 一定要知道自己的生命周期的,知道自己什么时候处于 Active 和 InActive 的状态。所以我们必须要给 databinding 设置一个正确的生命周期.

    下面来看一段有问题的代码:

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = HomeFragmentBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = this // 问题代码在这里!!!
        return binding.root
    }
    

    这段代码运行起来没有问题,看起来都是按照预期的在执行。甚至官方代码也是这么写的。连 LeakCanary 也检测不出来内存泄漏的问题,LeakCanary 只能检测出来一些 Activity,Fragment 和 View 等实例的内存泄漏,对于普通的类的实例是没有办法分析的。

    问题就出现在 databinding 遇到了一个错的 lifecycle,在没有用 Navigation 框架的时候,View 的生命周期和 Fragment 的生命周期一致的,但是在 Navigation 框架下,两者的生命周期是不一致的。我们来看下 ViewDataBinding 设置 lifecycleOwner 的具体代码。

    下面的代码中,往这个 lifecycleOwner 里面加入了一个 OnStartListener 实例,因为这个 lifecycleOwner 是 fragment 的,会在 fragment 销毁的时候反注册,但是并不会在 View 被销毁的时候被反注册。而 OnStartListener 有对这个 ViewDataBinding 有引用,会导致 View 被销毁的时候(跳到另外一个页面),这个引用会阻止系统回收这个 View。

    这个分析逻辑是对的,但是结果是不对的,系统还是会对这个 View 进行回收,因为 OnStartListener 的实例持有的是对这个 View 的弱引用,这个 View 还是会被回收。这就是 LeakCanary 没有报错的原因。但是这个 OnStartListener 的实例,就没这么幸运了,正是这个实例无法回收导致了内存泄漏。

    @MainThread
    public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
        if (mLifecycleOwner == lifecycleOwner) {
            return;
        }
        if (mLifecycleOwner != null) {
            mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
        }
        mLifecycleOwner = lifecycleOwner;
        if (lifecycleOwner != null) {
            if (mOnStartListener == null) {
                mOnStartListener = new OnStartListener(this);
                // 这个实例持有了ViewDataBinging的实例,虽然是弱引用。
            }
            lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
            // 问题出现在这里,如果这个lifecycle是fragment的,View被销毁了,里面不会进行反注册。
        }
        for (WeakListener<?> weakListener : mLocalFieldObservers) {
            if (weakListener != null) {
                weakListener.setLifecycleOwner(lifecycleOwner);
            }
        }
    }
    

    正确的做法是需要给这个 ViewDataBinding 设置 viewLifecycleOwner.

    binding.lifecycleOwner = viewLifecycleOwner
    

    多说一句啊,这个问题是如何被发现的呢?我们有一套检查框架逻辑的代码,对于这个问题,我们会在 fragment 的 onStop 函数里面检查有多少实例在监听 fragment 的生命周期,我们发现,这个数字会一直涨,这个问题就暴露了,适当的时候,和大家分享一下这套框架。

    3. Glide 自我管理的生命周期值得信赖吗?

    不值得信赖了

    Glide 是一个非常流行的图片加载框架,不得不说,Glide 的缓存这一块的设计非常的优秀,功能强大,可扩展强。还有它的生命周期的自我管理,通过创建一个 fragment 在当前的页面,通过这个 fragment 的生命周期,实现在 onStart 的时候进行图片加载,在 onStop 的时候,把还没有执行或者没有执行完的任务缓存下来,以便在 onStart 的再执行,当然是在没有 onDestory 的情况下。

    一切都很完美,直到遇到了 Navigation。

    glide.with(fragment).load(url).into(imageview).
    

    呵呵,上面的这段,在 Navigation 的架构下,如果 Fragment 还在,但是执行了 onDestroyView,imageview 需要被销毁。这个情况下,如果图片加载任务没执行完,任务就会被缓存下来了。这个任务还有对需要被销毁的 imageview 有强引用,导致这个 imageview 销毁不了,从而内存泄漏。

    如何 100%的重现这个问题呢,有个简单的方法,让大家可以验证一下这个问题。给这个任务,加一个图片的 transformation,这个 transformation 什么也不干,就是 sleep 3 秒钟,在这个 3 秒中之内,跳转到另一个页面。这会导致当前页面进行 View 的 destory,但是 fragment 并不会 destory,因为这个任务还没执行完,这个任务就会被 Glide 缓存,具体会被缓存位置为 RequestManager->RequestTracker->pendingRequests。

    如何来解决这个问题呢?这个没有现成的解决方法,在 Glide 的官网有提类似的问题,但是 Glide 维护者听起来还没有意识到这个问题,没有后续的计划。当然,我们需要来解决这个问题,不然我们的代码就会存在这一点瑕疵了。

    解决的方法:自己来管理 Glide 的生命周期,不要通过那个看不见的 fragment 的生命周期,因为那是靠不住的。我们自己写了一个 RequestManager,通过传入的 fragment 的 viewLifecycleOwner 来进行管理。使用也很方便,在调用的时候如下即可。

    KGlide.with(fragment).load(url).into(imageview).
    

    源码精简了一下,贴在这里,请指正。

    import com.bumptech.glide.manager.Lifecycle as GlideLifecycle
    
    class KGlide {
    
        companion object {
            private val lifecycleMap = ArrayMap<LifecycleOwner, RequestManager>()
    
            @MainThread
            fun with(fragment: Fragment): RequestManager {
                Util.assertMainThread()
    
                val lifecycleOwner = fragment.viewLifecycleOwner
                if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
                    throw IllegalStateException("View is already destroyed.")
                }
    
                if (lifecycleMap[lifecycleOwner] == null) {
                    val appContext = fragment.requireContext().applicationContext
                    lifecycleMap[lifecycleOwner] = RequestManager(
                        Glide.get(appContext),
                        KLifecycle(lifecycleOwner.lifecycle),
                        KEmptyRequestManagerTreeNode(), appContext
                    )
                }
                return lifecycleMap[lifecycleOwner]!!
            }
        }
    
        class KEmptyRequestManagerTreeNode : RequestManagerTreeNode {
            override fun getDescendants(): Set<RequestManager> {
                return emptySet()
            }
        }
    
        class KLifecycle(private val lifecycle: Lifecycle) : GlideLifecycle {
            private val lifecycleListeners =
                Collections.newSetFromMap(WeakHashMap<LifecycleListener, Boolean>())
    
            private val lifecycleObserver = object : DefaultLifecycleObserver {
                override fun onStart(owner: LifecycleOwner) {
                    val listeners = Util.getSnapshot(lifecycleListeners)
                    for (listener in listeners) {
                        listener.onStart()
                    }
                }
    
                override fun onStop(owner: LifecycleOwner) {
                    val listeners = Util.getSnapshot(lifecycleListeners)
                    for (listener in listeners) {
                        listener.onStop()
                    }
                }
    
                override fun onDestroy(owner: LifecycleOwner) {
                    val listeners = Util.getSnapshot(lifecycleListeners)
                    for (listener in listeners) {
                        listener.onDestroy()
                    }
    
                    lifecycleMap.remove(owner)
                    lifecycleListeners.clear()
                    lifecycle.removeObserver(this)
                }
            }
    
            init {
                lifecycle.addObserver(lifecycleObserver)
            }
    
            override fun addListener(listener: LifecycleListener) {
                lifecycleListeners.add(listener)
                when (lifecycle.currentState) {
                    Lifecycle.State.STARTED, Lifecycle.State.RESUMED -> listener.onStart()
                    Lifecycle.State.DESTROYED -> listener.onDestroy()
                    else -> listener.onStop()
                }
            }
    
            override fun removeListener(listener: LifecycleListener) {
                lifecycleListeners.remove(listener)
            }
        }
    }
    

    4. Android 组件的生命周期自我管理值得信任吗?

    不值得,信任需要我们对 Android 生命周期的管理细节足够的了解。没有足够的了解,哪里来的信任,也就是盲目的信任。

    我们在 Android 官方文档里面应该看到过 LiveData 的介绍,下面摘录一段。

    Livedata is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

    然后还向我们说明 Livedata 的不会导致内存泄漏。

    This is especially useful for activities and fragments because they can safely observe LiveData objects and not worry about leaks—activities and fragments are instantly unsubscribed when their lifecycles are destroyed.

    写的很清楚,言之昭昭啊。如果你相信了官方文档的介绍,就 too young,too simple 了。LiveData 未必会在 lifecycleOwner 销毁的时候进行反注册,内存泄漏还是会发生。我们看一段 LiveData 会产生内存泄漏的代码。

    class HomeFragment : Fragment() {
        private val model: NavigationViewModel by viewModels()
    
        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.home_fragment, container, false)
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            model.getTextValue().observe(viewLifecycleOwner){
                view.findViewById<Button>(R.id.text).text = it
            }
    
            if (isXXX()) {
                findNavController().navigate(R.id.next_action)
            }
        }
    }
    

    当你进入某个页面,发现需要导航到另一个页面,这个时候就需要很小心。如果像上面这样的写法,就会导致内存泄漏。

    这个 Case 里,在 Fragment.onViewCreated()的模板方法,监听了一个 LiveData,这会导致这个 LiveData 持有外面对象的引用。理想情况下,这个 LivaData 会在 LifecycleOwner 在 onDestory 的时候进行反注册,但是在一些情况下,这个反注册就不会进行。

    如上代码的情况下,如果这个页面马上跳到 next_action 的页面,之前订阅的 LiveData 就不会进行反注册。原因出在当跳出这个页面的时候,页面还处于生命周期的状态 INITIALIZED,但是反注册的条件是这个页面的生命周期状态至少是 CREATED.

    void performDestroyView() {
        mChildFragmentManager.dispatchDestroyView();
        if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
                        .isAtLeast(Lifecycle.State.CREATED)) {
            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
        }
        ......
    
    }
    

    其实 Android 的生命周期管理还是值得信任的,前提是我们得彻底搞清楚状态流转的细节。

    5. 当 ViewPager2 遇到 Navigation

    ViewPager 是在应用开发的过程中,高频的用到的组件。Android 的官网有对基本的使用有详细的介绍。

    一直都很美好,直到遇到 Navigation。

    让我们来看官方例子里面 ViewPager2 的 Adapter 的类的声明。

    class DemoCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    
        override fun getItemCount(): Int = 100
    
        override fun createFragment(position: Int): Fragment {
            // Return a NEW fragment instance in createFragment(int)
            val fragment = DemoObjectFragment()
            fragment.arguments = Bundle().apply {
                // Our object is just an integer :-P
                putInt(ARG_OBJECT, position + 1)
            }
            return fragment
        }
    }
    

    不避讳的说,我们实际项目中的代码,也犯了同样的问题。不是说官网的写法有问题,而是在 Navigation 的框架下,才会导致的内存泄漏问题。这个泄漏是如何发生的呢?我们来看一下 FragmentStateAdapter 的构造函数。

    /**
     * @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass.
     *
     * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
     * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)
     */
    public FragmentStateAdapter(@NonNull Fragment fragment) {
        this(fragment.getChildFragmentManager(), fragment.getLifecycle());
    }
    /**
     * @param fragmentManager of {@link ViewPager2}'s host
     * @param lifecycle of {@link ViewPager2}'s host
     *
     * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
     * @see FragmentStateAdapter#FragmentStateAdapter(Fragment)
     */
    public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
            @NonNull Lifecycle lifecycle) {
        mFragmentManager = fragmentManager;
        mLifecycle = lifecycle;
        super.setHasStableIds(true);
    }
    

    可以看到 FragmentStateAdapter 最终会走两个参数的构造函数。第一个是 fragmentManager of ViewPager2's Host,第二个参数是 lifecycle of ViewPager2's host。如果你看懂了之前的问题,你就会知道这个问题出在哪里了。在 Navigation 下面,fragment 和 view 的生命周期是不一致的,如果我们在 FragmentStateAdapter 的构造函数中,只传入 fragment 的实例的话,第二个参数 lifecycle 用的是第一个参数 fragment 的 lifecycle。但是很显然,viewpager2's host 的 lifecycleOwner 是 fragment 的 viewlifecycleOwner,而不是其本身。

    具体导致的问题是,在 ViewPager2 实例被销毁的时候,对应的 FragmentStateAdapter 并不会被销毁,因为如果只传一个参数的话,使用的是 Fragment 的生命周期,只有在 fragment 退出的时候,才会被销毁。

    这里多说一句啊,FragmentStateAdapter 实例不能被设置到多个 ViewPager2 的对象,所以当 ViewPager2 被重建的时候,这个 Adapter 不能被重用。

    这些问题其实很难被发现,LeakCanary 也不能发现。幸好我们有个工具,在每个需要被检查的类的构造函数里面进行记录,然后在类的 finalize 方法对这个记录进行处理,如果发现某个类一直被构造,但是不执行 finalize 方法,这个类就需要被好好关照了。

    6. ViewPager2 设置 Adapter 导致的 Fragment 重建问题

    先来看以下的代码片段:

    Line1:val viewPager2: ViewPager2 = ......
    Line2:val adapter: FragmentStateAdapter = ......
    Line3:viewPager2.adapter = adapter
    Line4:model.getContentList.observe(viewLifecycleOwner) {
    Line5:    adapter.data = it
    Line6:    adapter.notifyDataSetChanged()
    Line7:}
    

    大家应该看不出来这段代码的问题所在的吧,这个是非常常规的写法。当然这段代码在非 Navigation 的架构下面是没有问题的。但是如果在 Navigation 的架构下,就会有比较严重的问题了。

    说明一下问题出现的场景,如果用户先进入这个页面,执行上面代码,viewpager 正常显示。然后注意,重要的步骤来了,在这个页面上,导航到另外一个页面。那当前的这个页面会执行 fragment 的 onStop,注意并不会执行 onDestory。但是会执行 onDestoryView,也就是说 viewPager 将会被销毁,但是 fragment 被保留了。

    那如果重新回到这个页面会发生什么事情呢,之前 onStop 的 fragment 会执行 onStart,包括 Adatper 里面生成的 fragment 也会进行重建,并创建 View。

    出人意料的事情发生了,Adatper 里面的 fragment 在重建完成之后,立刻又被销毁掉了,这里的销毁是真正的销毁,执行了 onDestory 方法。然一个新的 fragment 被重新创建出来,这就是 fragment 重建问题。是什么导致了这个问题呢?

    具体执行销毁 Fragment 的代码如下,在 FragmentStateAdapter 的 gcFragments 的方法。

    void gcFragments() {
        if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {
            return;
        }
    
        // Remove Fragments for items that are no longer part of the data-set
        Set<Long> toRemove = new ArraySet<>();
        for (int ix = 0; ix < mFragments.size(); ix++) {
            long itemId = mFragments.keyAt(ix);
            if (!containsItem(itemId)) {
                toRemove.add(itemId);
                mItemIdToViewHolder.remove(itemId); // in case they're still bound
            }
        }
    
        // Remove Fragments that are not bound anywhere -- pending a grace period
        if (!mIsInGracePeriod) {
            mHasStaleFragments = false; // we've executed all GC checks
    
            for (int ix = 0; ix < mFragments.size(); ix++) {
                long itemId = mFragments.keyAt(ix);
                if (!isFragmentViewBound(itemId)) {
                    toRemove.add(itemId);
                }
            }
        }
    
        for (Long itemId : toRemove) {
            removeFragment(itemId);
        }
    }
    

    因为这个函数判断,之前 adapter 里面产生的 fragment 需要被回收,依据就是当前的 adatper.containsItem(id)的方法返回 false 了。再提供一个信息,这个函数会在 viewpager2 设置 adatper 的时候被调用。到现在为止,答案已经出来了。因为在 viewpager2 设置 adatper 的时候,adatper 里面什么数据也没有的啊,containsItem 函数必然返回为空的啊,真相大白了。

    所以逻辑正确的代码应该如下:

    val viewPager2: ViewPager2 = ......
    model.getContentList.observe(viewLifecycleOwner) {
        if(viewPager2.adapter == null){
            val adapter: FragmentStateAdapter = ......
            adapter.data = it
            viewPager2.adapter = adapter
        } else {
            viewPager2.adapter.data = it
        }
    
        adapter.notifyDataSetChanged()
    }
    

    这段代码解决问题就是,在 viewpager2 设置 adatper 之前,先把 adapter 填充进去数据,然后再进行设置。这样就可以解决在 gcFragments 里面因为 containsItem()函数返回 false,导致 fragment 被销毁的问题,其实这个 fragment 是可以被重用的。

    7. 在 Navigation 的框架下,手动进行 Fragment 管理需要注意什么?

    刚开始使用 Navigation,代码里还是会有一部分 Fragment 是手动管理的,通过 FragmentManager 的 Add/Replace/Remove 等操作。其实 Navigation 设计的一部分初衷,就是要用统一的导航操作来替代手动操作,虽然 Navigation 底层的操作也是通过 FragmentManager 来实现的。

    如果代码里面还是有手动管理的代码,需要特别注意就是手动操作的时机和方式。原因还是在 Navigation 的框架下,Fragment 和 View 的生命周期不一致导致的。如果把操作 Fragment 的时机放在 ViewLifeCycle 的里面,就可能会造成一些意想不到的结果。

    假设 stack top-most 的页面返回之后,新处于 Stack 顶的页面,原先处于 Stop 的 Fragment 就会走 onStart,而整个 View 将会被重建。如果在 ViewLifeCycle 的生命周期里面去 Add or Replace 一个 fragment,就必须要判断,需要操作的 fragment 是否已经存在,如果这个 fragment 已经存在,又进行了一次 Add or Replace 操作,这个 fragment 将会被重建,原来的 fragment 将会被销毁,新的 fragment 会被创建,而 view 的生命周期将会走两次,导致不必要的性能损失。不仅仅是性能的损失,还会导致之前谈到的第 4 条问题,导致内存泄漏的问题。

    即使是注意到了这些问题,如果手动来判断情况,也会造成代码不必要的复杂,所以还是建议使用 Navigation 的框架来导航,而不是手动通过 FragmentManager 来进行操作。

    8. Navigation 的主持下,Fragment 和 View 分家了,家产怎么分?

    最后其实是个设计问题。View 是依附于 Fragment,从 Fragment 的 create 到 destory 的一生中,可能会伴随着多个 View 的实例,从 create 走到 destory。

    从而,我们需要考虑,哪些变量应该是属于 fragment 的,哪些变量应该属于 view 的。

    举一个例子,如果一个页面有一个列表,使用 recycleView 来实现,毫无疑问,recycleView 是属于 View 的,但是这个 recycleView 的 adapter 呢?如果这个 adapter 属于 view,那 adapter 的实例将会随着这个 recycleview 的创建而创建,消亡而消亡。如果把这个 adapter 放到 fragment,不管有多少个 View 实例将会被创建,用的就是 fragment 里面的 adapter,这样的设计,是不是会更好,更少的代价来实现需求。

    所以这个家产如何分,第一优先顺位是 fragment, 第二个顺位才是 view。放在 fragment 里面才会最大程度的重用对象,达到性能的最大化。

    加入我们

    欢迎加入字节跳动移动 OS 团队,我们以移动 OS 为核心,致力于成为 OS 领域的创新者和探索者。我们为教育、办公、家庭等提供稳健的操作平台,赋能字节内部明星产品。深入钻研 OS 的改进和优化,探索 OS 领域的前沿技术;同时通过对系统优化的探索,将更稳定的系统应用于产品;深度剖析时下最热智能硬件产品,为互联网+智能硬件行业带来更多可能!

    移动 OS 团队正在热招 Android、iOS、服务器架构师和研发工程师,最 Nice 的工作氛围和成长机会,各种福利各种机遇,base 北京,欢迎投递简历!联系邮箱 ketian@bytedance.com ; 邮件标题:姓名-移动 OS-职位

    点个在看杀个 Bug ❤

    展开全文
  • 最近在研究侧滑菜单时发现了一些问题,如果你之前没有接触过肯定会去百度,而我也看了很多demo,相信大家看到的例子都...你可能会无从下手,那么只有看源码了,NavigationView中有inflateHeaderView这个方法,看到这...

    最近在研究侧滑菜单时发现了一些问题,如果你之前没有接触过肯定会去百度,而我也看了很多demo,相信大家看到的例子都是下面那样布局的

    headerLayout加载头布局,menu加载菜单,这样就组成了一个完整的菜单,那么问题来了,menu的点击事件网上都贴出来了,很简单,那么头部呢?你可能会无从下手,那么只有看源码了,NavigationView 中有inflateHeaderView这个方法,看到这个方法你肯定就会觉得是通过这个方法加载头布局,好吧,现在方法有了,但是当你通过这个方法加载时会发现菜单中出现了两个头布局,很显然是加载了两次,第一次就是在布局文件中指定了headerLayout,当你滑动菜单时就会加载这个头布局,第二次是你在代码中又加载了一次。所以会出现两个布局。只要将布局中的headerLayout那行代码删除就可以实现你要的效果。下面给出绑定头部布局的代码

    //布局文件

    android:layout_width="wrap_content"

    android:layout_height="match_parent"

    andriod:layout_gravity="start"

    app:headerLayout="@layout/drawer_header"

    app:menu="@menu/drawer"/>

    //绑定侧滑菜单headerlayout布局,

    View drawview = nav_view.inflateHeaderView(R.layout.view_leftmenu);

    ImageView user_pic = (ImageView) drawview.findViewById(R.id.imag_user_pic);

    通过NavigationView 来加载头布局后再进行控件绑定就可以解决问题。

    转载时请注明出处及相应链接,本文永久地址:https://blog.yayuanzi.com/14740.html

    75d087ef9a9fb11dc373caaf33adbf7f.png

    微信打赏

    支付宝打赏

    感谢您对作者abel的打赏,我们会更加努力!    如果您想成为作者,请点我

    展开全文
  • I currently have an Android application when in the process of browsing a hierarchy of objects creates a back stack like this.BrowseActivity(Starting Instance) -> BrowseActivity(Instance B) -> B...

    I currently have an Android application when in the process of browsing a hierarchy of objects creates a back stack like this.

    BrowseActivity(Starting Instance) -> BrowseActivity(Instance B) -> BrowseActivity(Instance C) -> ViewObjectActivity

    There is a menu button in the view object activity that I would like to have take the user back to BrowseActivity(Starting Instance) and destroy the rest of the browse activities.

    Using Intent.FLAG_ACTIVITY_CLEAR_TOP only destroys the ViewObjectActivity and leaves the rest in the back stack.

    Setting the BrowseActivity to "singleTop" in the manifest breaks the ability to create another instance for further navigation.

    Any way to accomplish this that I am probably overlooking?

    展开全文
  • AndroidNavigationA library managing nested Fragment, translucent StatusBar and Toolbar for Android.You could use it as a single Activity Architecture Component.This is also the subproject of react-nat...

    AndroidNavigation

    A library managing nested Fragment, translucent StatusBar and Toolbar for Android.

    You could use it as a single Activity Architecture Component.

    This is also the subproject of react-native-navigation-hybrid.

    特性

    一行代码实现 Fragment 嵌套,一次性构建好嵌套层级

    一行代码实现 Fragment 跳转,不再需要写一大堆操作 fragment 的代码了,不用担心用错 FragmentManager 了

    可扩展性强,允许自定义容器和路由

    自动为你创建 Toolbar,一行代码设置标题、按钮,支持关闭自动创建功能以实现定制

    一处设置全局样式,到处使用,并且支持不同页面个性化

    支持侧滑返回

    支持懒加载

    支持 font icons

    6.0 screenshot:

    a49ec21c2dc9f7f92b54d5d5bb1ed596.png

    0aa554bd62ede2d21492d738effa1f97.png

    Installation

    implementation 'io.github.listenzz:AndroidNavigation:9.0.0'

    implementation 'androidx.appcompat:appcompat:1.2.0'

    allprojects {

    repositories {

    google()

    mavenCentral()

    }

    }

    Usage

    构建 UI 层级

    你的 Fragment 需要继承 AwesomeFragment。

    你的 Activity 需要继承 AwesomeActivity,然后设置 rootFragment。

    public class MainActivity extends AwesomeActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {

    TestFragment testFragment = new TestFragment();

    setActivityRootFragment(testFragment);

    }

    }

    }

    你可以调用 setActivityRootFragment 多次,根据不同的 App 状态展示不同的根页面。比如一开始你只需要展示个登录页面,登陆成功后将根页面设置成主页面。

    AwesomeFragment 同样部署了 setActivityRootFragment 接口,方便你随时随地切换 activity 的根。

    你通常还需要另外一个 Activity 来做为闪屏页(Splash),这个页面则不必继承 AwesomeActivity。

    为了处理常见的 Fragment 嵌套问题,提供了 NavigationFragment、TabBarFragment、DrawerFragment 三个容器类。它们可以作为 Activity 的 rootFragment 使用。这三个容器为 Fragment 嵌套提供了非常便利的操作。

    NavigationFragment

    NavigationFragment 以栈的形式管理它的子 Fragment,支持 push、pop 等操作,在初始化时,需要为它指定 rootFragment。

    public class MainActivity extends AwesomeActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {

    TestFragment testFragment = new TestFragment();

    NavigationFragment navigationFragment = new NavigationFragment();

    // 把 TestFragment 设置为 NavigationFragment 的根

    navigationFragment.setRootFragment(testFragment);

    // 把 NavigationFragment 设置为 Activity 的根

    setActivityRootFragment(navigationFragment);

    }

    }

    }

    如果 TestFragment 的根布局是 LinearLayout 或 FrameLayout,会自动帮你创建 Toolbar,当由 A 页面跳转到 B 页面时,会为 B 页面的 Toolbar 添加返回按钮。更多关于 Toolbar 的配置,请参考 设置 Toolbar 一章。

    在 TestFragment 中,我们可以通过 getNavigationFragment 来获取套在它外面的 NavigationFragment,然后通过 NavigationFragment 提供的 pushFragment 跳转到其它页面,或通过 popFragment 返回到前一个页面。关于导航的更多细节,请参考 导航 一章。

    TabBarFragment

    这也是一个比较常见的容器,一般 APP 主界面底下都会有几个 tab,点击不同的 tab 就切换到不同的界面。

    public class MainActivity extends AwesomeActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {

    // 首页

    HomeFragment homeFragment = new HomeFragment();

    homeFragment.setTabBarItem(new TabBarItem("首页", R.drawable.icon_home));

    // 通讯录

    ContactsFragment contactsFragment = new ContactsFragment();

    contactsFragment.setTabBarItem(new TabBarItem("通讯录", R.drawable.icon_contacts));

    // 添加 tab 到 TabBarFragment

    TabBarFragment tabBarFragment = new TabBarFragment();

    tabBarFragment.setFragments(homeFragment, contactsFragment);

    // 把 TabBarFragment 设置为 Activity 的根

    setActivityRootFragment(tabBarFragment);

    }

    }

    }

    在 HomeFragment 或 ContactsFragment 中,可以通过 getTabBarFragment 来获取它们所属的 TabBarFragment.

    可以通过 TabBarFragment 的 setSelectedIndex 方法来动态切换 tab,通过 getTabBar 可以获取 TabBar, 然后可以调用 TabBar 提供的方法来设置红点,未读消息数等。

    如果对提供的默认 TabBar 不满意,可以通过实现 TabBarProvider 来自定义 TabBar , 在设置 TabBarFragment 为其它容器的根前,调用 TabBarFragment#setTabBarProvider 来设置自定义的 TabBar, 参数可以为 null, 表示不需要 TabBar.

    如果 HomeFragment 或 ContactsFragment 需要有导航的能力,可以先把它们嵌套到 NavigationFragment 中。

    public class MainActivity extends AwesomeActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {

    // 首页

    HomeFragment homeFragment = new HomeFragment();

    NavigationFragment homeNavigationFragment = new NavigationFragment();

    homeNavigationFragment.setRootFragment(homeFragment);

    homeNavigationFragment.setTabBarItem(new TabBarItem("首页", R.drawable.icon_home));

    // 通讯录

    ContactsFragment contactsFragment = new ContactsFragment();

    NavigationFragment contactsNavigationFragment = new NavigationFragment();

    contactsNavigationFragment.setRootFragment(contactsFragment);

    contactsNavigationFragment.setTabBarItem(new TabBarItem("通讯录", R.drawable.icon_contacts));

    // 添加 tab 到 TabBarFragment

    TabBarFragment tabBarFragment = new TabBarFragment();

    tabBarFragment.setFragments(homeNavigationFragment, contactsNavigationFragment);

    // 把 TabBarFragment 设置为 Activity 的根

    setActivityRootFragment(tabBarFragment);

    }

    }

    }

    DrawerFragment

    这个容器内部封装了 DrawerLayout。使用时需要为它设置两个子 Fragment。

    public class MainActivity extends AwesomeActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {

    DrawerFragment drawerFragment = new DrawerFragment();

    drawerFragment.setContentFragment(new ContentFragment());

    drawerFragment.setMenuFragment(new MenuFragment());

    // 把 drawerFragment 设置为 Activity 的根

    setActivityRootFragment(drawerFragment);

    }

    }

    }

    在 ContentFragment 或 MenuFragment 中,我们可以通过 getDrawerFragment 来获取它们所属的 DrawerFragment。

    DrawerFragment 提供了 toggleMenu、openMenu、closeMenu 这几个方法来打开或关闭 Menu。

    可以通过 getContentFragment、getMenuFragment 来获取对应的 Fragment。

    可以通过 setMinDrawerMargin 或 setMaxDrawerWidth 来设置 menu 的宽度

    contentFragment 可以是一个像 TabBarFragment 这样的容器。可以参考 demo 中 MainActivity 中的设置。

    自定义容器

    如果以上容器都不能满足你的需求,你可以自定义容器。

    容器在添加子 fragment 时一定要注意判断 savedInstanceState 是否为 null, 会不会在生命周期重启时,重复添加 fragment。

    可以参考 demo 中 ViewPagerFragment 这个类,它就是个自定义容器。

    自定义容器,继承 AwesomeFragment 并重写下面这个方法。

    @Override

    public boolean isParentFragment() {

    return true;

    }

    因为 AwesomeFragment 会为非容器类 Fragment 的 root view 添加背景。如果容器不表明它是容器,也会为容器添加背景,这样就会导致不必要的 overdraw。

    可能需要有选择地重写以下方法

    @Override

    protected AwesomeFragment childFragmentForAppearance() {

    // 这个方法用来控制当前的 status bar 的样式是由哪个子 fragment 决定的

    // 如果不重写,则由容器自身决定

    // 可以参考 NavigationFragment、TabBarFragment

    // 是如何决定让哪个子 fragment 来决定 status bar 样式的

    return 一个恰当的子 fragment;

    }

    如何使不同 fragment 拥有不同的 status bar 样式,请参考 设置状态栏 一章

    @Override

    protected boolean onBackPressed() {

    // 这个方法用来控制当用户点击返回键时,到底要退出哪个子 fragment

    // 返回 true 表示当前容器消费了此事件,否则转发给上一层容器处理

    // 可以参考 DrawerFragment,NavigationFragment 是如何处理返回键的

    return super.onBackPressed();

    }

    非容器页面也可以重写 onBackPressed 来处理用户点击返回按钮事件。

    所见即所得 Dialog

    Fragment 可以作为 Dialog 显示,本库做了特殊处理,使得显示出来的 Dialog 布局和在 xml 预览中所见一模一样。实现细节请看这篇文章。

    导航

    导航是指页面间的跳转和传值,实际上和容器如何管理它的子 Fragment 有很大关系。

    present & dismiss

    AwesomeActivity 和 AwesomeFragment 提供了两个基础的导航功能 present 和 dismiss

    present

    present 是一种模态交互方式,只有关闭被 present 的页面,才可以回到上一个页面,通常要求 presented 的页面给 presenting 的页面返回结果,类似于 startActivityForResult。

    比如 A 页面 present 出 B 页面

    // A.java

    presentFragment(testFragment, REQUEST_CODE);

    B 页面返回结果给 A 页面

    // B.java

    Bundle result = new Bundle();

    result.putString("text", resultEditText.getText().toString());

    setResult(Activity.RESULT_OK, result);

    dismissFragment();

    A 页面实现 onFragmentResult 来接收这个结果

    // A.java

    @Override

    public void onFragmentResult(int requestCode, int resultCode, Bundle data) {

    super.onFragmentResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_CODE) {

    if (resultCode != 0) {

    String text = data.getString("text", "");

    resultText.setText("present result:" + text);

    } else {

    resultText.setText("ACTION CANCEL");

    }

    }

    }

    有些时候,比如选择一张照片,我们先要跳到相册列表页面,然后进入某个相册选择相片返回。这也是没有问题的。 A 页面 present 出相册列表页面

    //AFragment.java

    NavigationFragment navigationFragment = new NavigationFragment();

    AlbumListFragment albumListFragment = new AlbumListFragment();

    navigationFragment.setRootFragment(albumListFragment);

    presentFragment(navigationFragment, 1)

    相册列表页面 push 到某个相册

    push 是 NavigationFragment 的能力,要使用这个功能,你的 fragment 外层必须有一个 NavigationFragment 做为容器。

    // AlbumListFragment.java

    AlbumFragment albumFragment = new AlbumFragment();

    getNavigationFragment.pushFragment(albumFragment);

    在相册页面选好相片后返回结果给 A 页面

    // AlbumFragment.java

    Bundle result = new Bundle();

    result.putString("uri", "file://...");

    setResult(Activity.RESULT_OK, result);

    dismissFragment();

    在 A 页面接收返回的结果(略)。

    dismiss

    关闭 present 出来的 Fragment,可以在该 Fragment 的任意子 Fragment 中调用,请参看上面相册的例子。

    present 所使用的 FragmentManager 是 Activity 的 getSupportFragmentManager,因此 present 出来的 fragment 是属于 Activity 的,它不属于任何 fragment 的子 fragment,这样就确保了 present 出来的 fragment 是模态的。

    showDialog

    把一个 fragment 作为 dialog 显示。showDialog 的参数列表和 present 是一样的,使用方式也基本相同。作为 dialog 的 fragment 可以通过 setResult 返回结果给把它作为 dialog show 出来的那个 fragment。

    hideDialog

    关闭作为 dialog 的 fragment

    onBackPressed

    通过重写该方法,并返回 true,可以拦截返回键事件。

    NavigationFragment

    NavigationFragment 是个容器,以栈的方式管理子 fragment,支持 push、pop、popTo、popToRoot 操作,并额外支持 redirectTo 操作。

    我们可以在它的子 Fragment 中(不必是直接子 fragment,可以是子 fragment 的子 fragment)通过 getNavigationFragment 来获取它的引用。

    在初始化 NavigationFragment 时,你必须调用 setRootFragment 来指定它的根页面。

    push

    入栈一个页面

    比如由 A 页面跳转到 B 页面。

    A -> B

    // AFragment.java

    BFragment bFragment = new BFragment();

    getNavigationFragment.pushFragment(bFragment);

    pop

    出栈一个页面

    比如你由 A 页面一路 push 到 D 页面

    A -> B -> C -> D

    现在想返回到 C 页面

    getNavigationFragment.popFragment();

    执行上述代码后,栈里面剩下 A B C 三个页面

    手势返回

    手势返回是 NavigationFragment 的能力,需要在 Activity 的 onCustomStyle 中开启。

    手势返回实质上是个 pop。

    popToRoot

    出栈直到栈底

    比如你由 A 页面一路 push 到 D 页面

    A -> B -> C -> D

    现在想返回到 A 页面

    getNavigationFragment.popToRootFragment();

    执行上述代码后,栈里面只剩下 A 页面

    popTo

    出栈,返回到之前的某个页面。

    比如你由 A 页面一路 push 到 D 页面

    A -> B -> C -> D

    现在想返回到 B 页面

    NavigationFragment navigationFragment = getNavigationFragment();

    if (navigationFragment != null) {

    AwesomeFragment target = FragmentHelper.findAwesomeFragment(requireFragmentManager(), BFragment.class);

    if (target != null) {

    navigationFragment.popToFragment(target);

    }

    }

    执行上述代码后,栈里面剩下 A B 两个页面

    你可能已经猜到,pop 和 popToRoot 都是通过 popTo 来实现的。pop 的时候也可以通过 setResult 设置返回值,不过此时 requestCode 的值总是 0。

    redirectTo

    重定向,出栈然后入栈。

    比如你由 A 页面一路 push 到 D 页面

    A -> B -> C -> D

    现在想用 E 页面替换 D 页面

    EFragment eFragment = new EFragment();

    getNavigationFragment().redirectToFragment(eFragment);

    执行上述代码后,栈里面有 A B C E 四个页面,D 页面被 E 页面所取代

    又比如你由 A 页面一路 push 到 D 页面

    A -> B -> C -> D

    现在想用 E 页面替换 B C D 三个页面

    NavigationFragment navigationFragment = getNavigationFragment();

    if (navigationFragment != null) {

    AwesomeFragment from = FragmentHelper.findAwesomeFragment(requireFragmentManager(), BFragment.class);

    navigationFragment.redirectToFragment(new EFragment(), from, true);

    }

    执行上述代码后,栈里面只有 A E 两个页面

    isNavigationRoot

    通过这个方法,可以判断当前 fragment 是不是 NavigationFragment 的 rootFragment

    上面这些操作所使用的 FragmentManager,是 NavigationFragment 的 getChildFragmentManager,所有出栈或入栈的 fragment 都是 NavigationFragment 的子 fragment.

    navigation_stack.png

    如上图,A fragment 嵌套在 NavigationFragment 中,A1 fragment 嵌套在 A fragment 中,当我们从 A1 push B fragment 时,B fragment 会成为 NavigationFragment 的子 fragment,而不是 A 的子 fragment,它和 A 是兄弟,它是 A1 的叔叔。

    自定义导航

    虽然 AwesomeFragment 和 NavigationFragment 提供的导航操作已经能满足大部分需求,但有时我们可能需要自定义导航操作,尤其是自定义容器的时候。

    需要注意几个点

    选择合适的 FragmentManager

    Activity#getSupportFragmentManager 会将 fragment 添加到 activity

    Fragment#getFragmentManager 拿到的是上一级的 fragmentManager, 通过它添加的 fragment 会成为当前 fragment 的兄弟。

    Fragment#getChildFragmentManager 会将 fragment 添加为当前 fragment 的子 fragment。

    设置正确的 tag

    总是使用有三个参数的 add 方法,最后一个 tag 传入目标 fragment 的 getSceneId 的值。

    正确使用 addToBackStack

    如果需要添加到返回栈,tag 参数不能为 null, 必须和传递给 add 或 replace 的 tag 一致,也就是目标 fragment 的 getSceneId 的值。

    如果不通过栈的形式来管理子 fragment 时,必须将当前子 fragment 设置为 primaryNavigationFragment

    参考 TabBarFragment 和 DrawerFragment,它们就不是用栈的形式管理子 fragment.

    getFragmentManager().setPrimaryNavigationFragment(fragment);

    一个容器中的子 fragment 要不都添加到返回栈中,就像 NavigationFragment 那样,要不都不添加到返回栈中,就像 TabBarFragment 和 DrawerFragment 那样,切勿混用这两种模式。

    FragmentTransaction#addSharedElement、FragmentTransaction#setTransition、FragmentTransaction#setCustomAnimations 不可以同时使用,其中 setTransition 要配合 AwesomeFragment#setAnimation 一起使用

    可以参考 demo 中 GridFragment 这个类,看如何实现自定义导航的,它遵循了 NavigationFragment 管理子 fragment 的规则。

    c35b6ddfa2d70263e3994b9c2f1cf23d.gif

    // 如果是通过异步回调的方式来触发转场,以下代码需要包裹在 scheduleTaskAtStarted 中

    // 将要显示的 Fragment 是 this 的兄弟

    requireFragmentManager()

    .beginTransaction()

    // 很重要

    .setReorderingAllowed(true)

    // 因为开启了共享元素转场,就不要设置 FragmentTransaction#setTransition 或者 FragmentTransaction#setCustomAnimations 了

    .addSharedElement(holder.image, "kittenImage")

    // 在添加新的 Fragment 之前先隐藏旧的

    .hide(this)

    // 使当前 fragment 处于 pause 状态

    .setMaxLifecycle(this, Lifecycle.State.STARTED)

    // 使用具有三个参数的 add

    .add(R.id.navigation_content, kittenDetails, kittenDetails.getSceneId())

    // 因为 NavigationFragment 以栈的形式管理子 Fragment

    .addToBackStack(kittenDetails.getSceneId()/*important*/)

    // 使用 commit 而不是 commitAllowingStateLoss 是个好习惯

    .commit();

    懒加载

    本库 5.0.0 以上支持使用 onResume 和 onPause 实现懒加载

    建议使用 ViewPager2 来代替 ViewPager

    全局样式设置

    可以通过重写 AwesomeActivity 如下方法来定制该 activity 下所有 fragment 的样式

    @Override

    protected void onCustomStyle(Style style) {

    }

    可配置项如下:

    {

    screenBackgroundColor: int // 页面背景,默认是白色

    statusBarStyle: BarStyle // 状态栏和 toolbar 前景色,可选值有 DarkContent 和 LightContent

    statusBarColor: String // 状态栏背景色,仅对 4.4 以上版本生效, 默认值是 colorPrimaryDark

    navigationBarColor: Integer. // 导航栏颜色,仅对 Android O 以上版本生效,建议保留默认设置

    toolbarBackgroundColor: int // toolbar 背景颜色,默认值是 colorPrimary

    elevation: int // toolbar 阴影高度, 仅对 5.0 以上版本生效,默认值为 4 dp

    shadow: Drawable // toolbar 阴影图片,仅对 4.4 以下版本生效

    backIcon: Drawable // 返回按钮图标,默认是个箭头

    toolbarTintColor: int // toolbar 按钮的颜色,默认根据 statusBarStyle 来推算

    titleTextColor: int // toolbar 标题颜色,默认根据 statusBarStyle 来推算

    titleTextSize: int // toolbar 标题字体大小,默认是 17 dp

    titleGravity: int // toolbar 标题的位置,默认是 Gravity.START

    toolbarButtonTextSize: int // toolbar 按钮字体大小,默认是 15 dp

    swipeBackEnabled: boolean. // 是否支持手势返回,默认是 false

    badgeColor: String // Badge 背景颜色

    // BottomBar

    tabBarBackgroundColor: String // TabBar 背景,默认值是 #FFFFFF

    tabBarShadow: Drawable // TabBar 分割线

    tabBarItemColor: String // TabBarItem 颜色,当 tabBarSelectedItemColor 未设置时,该值为选中时的颜色,否则为未选中时的颜色

    tabBarSelectedItemColor: String // TabBarItem 选中时的颜色

    }

    所有的可配置项都是可选的。

    如果某个 fragment 与众不同,可以为该 fragment 单独设置样式,只要重写该 fragment 的 onCustomStyle 方法,在其中设置那些不同的样式即可。

    设置状态栏

    4b92d4fe90cca73051c45b1165910e24.gif

    设置方式非常简单,重写 AwesomeFragment 中的 onCustomStyle 方法即可。

    @Override

    protected void onCustomStyle(@NonNull Style style) {

    super.onCustomStyle(style);

    style.setToolbarTintColor(Color.WHITE);

    style.setToolbarBackgroundColor(Color.TRANSPARENT);

    style.setStatusBarStyle(BarStyle.LightContent);

    style.setStatusBarColor(Color.TRANSPARENT);

    }

    或者通过重写以下方法,返回期望值:

    // AwesomeFragment.java

    protected BarStyle preferredStatusBarStyle();

    protected boolean preferredStatusBarHidden();

    protected int preferredStatusBarColor();

    protected boolean preferredStatusBarColorAnimated();

    preferredStatusBarStyle

    默认的返回值是全局样式的 style.getStatusBarStyle()。

    BarStyle 是个枚举,有两个值。LightContent 表示状态栏文字是白色,如果你想把状态栏文字变成黑色,你需要使用 DarkContent。

    仅对 6.0 以上版本生效

    preferredStatusBarHidden

    状态栏是否隐藏,默认是不隐藏。如果你需要隐藏状态栏,重写这个方法,把返回值改为 true 即可。

    preferredStatusBarColor

    状态栏的颜色,默认是全局样式 style.getStatusBarColor(),如果某个页面比较特殊,重写该方法,返回期待的颜色值即可。

    preferredStatusBarColorAnimated

    当状态栏的颜色由其它颜色转变成当前页面所期待的颜色时,需不需要对颜色做过渡动画,默认是 true,使得过渡更自然。如果过渡到某个界面状态栏出现闪烁,你需要在目标页面关闭它。参考 demo 中 TopDialogFragment 这个类。

    如果你当前页面的状态栏样式不是固定的,需要根据 App 的不同状态展示不同的样式,你可以在上面这些方法中返回一个变量,当这个变量的值发生变化时,你需要手动调用 setNeedsStatusBarAppearanceUpdate 来通知框架更新状态栏样式。可以参考 demo 中 CustomStatusBarFragment 这个类。

    设置 Toolbar

    当 fragment 的 parent fragment 是一个 NavigationFragment 时,会自动为该 fragment 创建 Toolbar。

    当 Fragment 的根布局是 LinearLayout 时,Toolbar 作为 LinearLayout 的第一个子元素添加。当 Fragment 的根布局是 FrameLayout 时,Toolbar 作为 FrameLayout 的最后一个子元素添加,覆盖在其余子元素最上面。

    你可以调用 AwesomeFragment 的以下方法来设置 Toolbar

    setTitle

    设置 Toolbar 标题

    setLeftBarButtonItem

    设置 Toolbar 左侧按钮

    setLeftBarButtonItems

    为左侧设置多个按钮时,使用此方法

    setRightBarButtonItem

    设置 Toolbar 右侧按钮,

    setRightBarButtonItems

    为右侧设置多个按钮时,使用此方法

    当然,你也可以设置 Menu

    Menu menu = getToolbar().getMenu();

    MenuItem menuItem = menu.add(title);

    menuItem.setIcon(icon);

    menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);

    menuItem.setOnMenuItemClickListener();

    请在 onActivityCreated 中调用上面这些方法

    Toolbar 的创建时机是在 Fragment onViewCreated 这个生命周期函数中,在此之前之前,调用 getAwesomeToolbar 得到的返回值为 null。

    如果当前 fragment 不是 NavigationFragment 的 rootFragment,会自动在 Toolbar 上创建返回按钮。如果你不希望当前页面有返回按钮,可以重写以下方法。

    protected boolean shouldHideBackButton() {

    return true;

    }

    如果你希望禁止用户通过返回键(物理的或虚拟的)或者手势退出当前页面,你可以重写以下方法,并返回 false。

    protected boolean isBackInteractive() {

    return false;

    }

    如果只是希望禁止用户通过手势退出当前页面,重写以下方法,返回 false,此时用户仍然可以通过返回键退出当前页面。

    protected boolean isSwipeBackEnabled() {

    return false;

    }

    如果你不希望自动为你创建 toolbar, 或者自动创建的 toolbar 所在 UI 层级不合适,你可以重写以下方法,返回 null 或者自定义的 toolbar。

    protected AwesomeToolbar onCreateAwesomeToolbar(View parent) {

    return null;

    }

    demo 中,NoToolbarFragment 返回 null, 表示不需要创建 toolbar。如果需要自定义 toolbar,请优先考虑基于 AwesomeToolbar 进行自定义,并在 onCreateAwesomeToolbar 返回自定义的 toolbar,就像 CoordinatorFragment 和 ViewPagerFragment 所做的那样。

    你还可以重写 onCustomStyle 这个方法,来修改 toolbar 的样式。

    @Override

    protected void onCustomStyle(@NonNull Style style) {

    super.onCustomStyle(style);

    style.setToolbarTintColor(Color.WHITE);

    style.setToolbarBackgroundColor(Color.TRANSPARENT);

    style.setStatusBarStyle(BarStyle.LightContent);

    style.setStatusBarColor(Color.TRANSPARENT);

    }

    如果开启了沉浸式,那么需要使用 appendStatusBarPadding 这个方法来给恰当的 view 添加 padding,请参考上面说到的那两个类。

    设置导航栏(虚拟键)

    仅对 Android 8 以上版本生效

    preferredNavigationBarColor

    建议保留默认设置,只在个别页面微调。

    导航栏背景默认规则如下:

    含「底部 Tab」的页面,虚拟键设置为「底部 Tab」的颜色

    不含「底部 Tab」的页面,默认使用页面背景颜色,也就是 style.getScreenBackgroundColor() 的值

    dialog 为透明色,但如果 dialog 的 animationType 设置为 slide, 则使用 activity 当前 navigation bar 的颜色

    如果页面含有复杂背景/纹理,建议设置为透明,这需要开发者自行覆写 preferredNavigationBarColor。

    preferredNavigationBarStyle

    自动根据 preferredNavigationBarColor 的值计算,如有特别需要,可覆盖该方法。

    使用 font icons

    把你的 font icon 文件放到 assets/fonts 目录中,就像 demo 所做的那样。每个图标会有一个可读的 name, 以及一个 code point,我们通常通过 name 来查询 code point,当然也可以人肉查好后直接使用 code point,demo 中就是这样。

    以下方法可以通过 code point 获取 glyph(字形)

    public static String fromCharCode(int... codePoints) {

    return new String(codePoints, 0, codePoints.length);

    }

    获取 glyph 后构建如下格式的 uri

    font://fontName/glyph/size/color

    其中 fontName 就是你放在 assets/fonts 文件夹中的字体文件名,但不包括后缀。size 是字体大小,如 24,color 是字体颜色,可选,只支持 RRGGBB 格式。

    可以参考 demo 中 MainActivity 中是怎样构建一个 fontUri 的。

    代码规范

    在 onActivityCreated 中配置和 Toolbar 相关的东西,比如设置标题、按钮。

    永远通过以下方式来获取 arguments, 否则后果很严重

    获取

    Bundle args = FragmentHelper.getArguments(fragment);

    设置

    TargetFragment target = new TargetFragment();

    Bundle args = FragmentHelper.getArguments(target);

    args.putInt("id", 1);

    getNavigationFragment().pushFragment(target);

    展开全文
  • Android Navigation使用

    2021-05-26 15:05:25
    文章目录Jetpack Navigation概述添加依赖Navigation说明导航图Fragment容器导航控制者...Navigation是一个可简化Android导航的库和插件 Navigation通过管理Fragment的切换,可以通过可视化的方式,看见App的交互流程
  • @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);... BottomNavigationView bottomNavigationView = findViewById((R.id.bottomNavigationView))...
  • navigation 组件 是 Android Jetpack重要组成部分,推出3年左右,2018谷歌I/O大会也曾介绍过。主要用于组织Fragment,通过Fragment来实现不同内容片段的显示。简化了Activity与多Fragment之间的切换逻辑代码。 ...
  • android navigation drawer

    热门讨论 2013-12-12 19:50:48
    android navigation drawer
  • Android Navigation 异常:java.lang.RuntimeException: Unable to start activity ComponentInfo{...MainActivity}: java.lang.IllegalStateException: Activity ***..**.MainActivity@*** does not have a ...
  • NavigationView头部设置监听事件的方法,供大家参考,具体内容如下1、将XML里的静态引入删除:2、在代码里 findViewById 找到 NavigationView,然后引入 Header 和 Menu,再获取头部布局,最后设置监听事件://...
  • 底部导航栏一、效果图二、实现1、创建Fragment以及布局文件2、添加FragmentContainerView和BottomNavigationView两个控件3、配置xml资源文件4、给BottomNavigationView配置NavController 一、效果图 使用jetpack的...
  • AndroidNavigation的使用一、页面导航:A-->B-->C 一、页面导航:A–>B–>C 页面从A到B,有B到C,然后由C返回A时的写法 页面B在xml中的配置 <fragment android:id="@+id/B" android:name="xxx....
  • 我正在使用Designs支持库NavigationView,如下所示:xmlns:app=...android:fits...
  • androidNavigation组件的Fragment间传递参数 说明 HomeFragment中点击某个按钮,传递参数到RichTextFragment 一、navigation中nav_home.xml中的配置 HomeFragment的配置: <fragment android:id="@+id/...
  • 1.抽屉布局(drawerlayout)activity_...拖入NavigationView组件(在container中)3.给NavigationView组件设置头部布局<1>在app--->src--->main-->res-->layout中新建NavigationView组件headlayout...
  • 场景问题: 使用BottomNavigationView+Navigation进行底部导航栏页面切换时,每次点击都会重新...2.修改xml之前对androidx.navigation.fragment.NavHostFragment的引用,改为重写的NavHostFragment 浅析问题所在:
  • View drawerView = navigationView.inflateHeaderView(R.layout.nav_header_main); CircleImageView account = (CircleImageView) drawerView.findViewById(R.id.account); account.setOnClickLis
  • Navigation组件,参数传递以及自定义动画
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~NavigationView菜单项增加分割线其实这篇文章有点短,短的可能一句话就说完了,于是就多扯点。~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
  • NavigationView 在 design 库中,添加依赖(最新的是 23.2.0);compile 'com.android.support:design:23.1.1'2. 然后在 DrawerLayout 布局中添加 NavigationView ;android:id="@+id/drawer_layout"xmlns:android=...
  • 界面2.2、在Navigation中添加切换关系android:id="@+id/FirstFragment"android:name="com.example.baogutou.FirstFragment"android:label="@string/first_fragment_label"tools:layout="@layout/fragm...
  • 文章目录AndroidNavigation在目的地之间传递数据、ViewModel一、在目的地之间传递数据1. 定义目的地参数2.使用 Safe Args 传递安全的数据3.在目的地之间添加动画过渡效果二、ViewModel1.实现 ViewModel2....
  • 更新:从支持库的22.2.1版本开始,此问题已得到解决,不再需要使用下面提到的解决方法保存/恢复视图状态不要使用NavigationView.SavedStateNavigationView是将saveEnabled设置为true的视图.这意味着视图本身应在内部...
  • 我有多个组的NavigationView,它基于我需要...我的示例NavigationView菜单android:id="@+id/grp_employee">android:id="@+id/help"android:checked="false"android:icon="@drawable/ic_help"android:title="@str...
  • NavigationDrawer(导航抽屉)是一个从屏幕的左边缘过渡并且显示app的主要导航操作的面板。显示Navigation Drawer用户能够从屏幕左边缘滑动滑块或者点击app的ActionBar上的图标调使Navigation Drawer显示到屏幕。作为...
  • Android Navigation TabBar控件实现多彩标签栏先看看效果图:源码下载:Android Navigation TabBar控件实现多彩标签栏代码:MainActivity.javapackage com.bzu.gxs.meunguide;import android.app.Activity;import ...
  • 我有一个嵌套在DrawerLayout中的NavigationView.activity_main.xml中android:id="@+id/drawer_layout"xmlns:android=...
  • 先看看效果图:代码:MainActivity.javapackage ...import android.app.Activity;import android.graphics.Color;import android.support.v4.view.PagerAdapter;import android.support.v4.view.ViewPager;...
  • Android MVVM框架搭建(五)前言正文一、 前言   MVVM框架的模式在这几篇文章中相比你已经熟悉很多了,具体的架构模式如下图所示: 上层的Activity/Fragment表示为View层,通过ViewModel去操作数据,然后由...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 44,460
精华内容 17,784
关键字:

androidnavigation