Navigation深入淺出绩社,到出神入化摔蓝,再到實(shí)戰(zhàn)改造(二)

了解Navigation使用后,思考幾個(gè)問題

  1. NavHostFragmnet作為路由容器愉耙,是如何解析nav_graph資源文件贮尉,從而生成NavGraph對(duì)象?
  2. 跳轉(zhuǎn)時(shí)朴沿,路由是如何被執(zhí)行的猜谚?
  3. 跳轉(zhuǎn)的路由目標(biāo)節(jié)點(diǎn),NavDestination又是如何創(chuàng)建的赌渣。
  4. 分析后是否能總結(jié)出Navigation的優(yōu)點(diǎn)和痛點(diǎn)
  5. 能否解決痛點(diǎn)魏铅,該如何解決,有什么思路锡垄?

源碼分析從下面的圖入手:

版本:
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
Navigation分析圖.png

     <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

核心類介紹:
NavHostFragment: 所有節(jié)點(diǎn)的外部容器

NavController: 外部調(diào)用的入口沦零,提供路由,回退等核心操作

NavDestination 節(jié)點(diǎn)的封裝類對(duì)應(yīng)nav_graph.xml文件中的 </navigation>货岭, </fragment> </activity>路操, </dialog>目標(biāo)節(jié)點(diǎn)(即Destination),同時(shí)有如四個(gè)子類:NavGraph千贯,F(xiàn)ragmentNavigator#Destination屯仗,ActivityNavigator#Destination,DialogFragmentNavigator#Destination

NavGraph 特殊的Destination搔谴,將app:navGraph="@navigation/nav_graph解析封裝成NavGraph對(duì)象魁袜,里面包含nav_graph.xml中的所有信息。根節(jié)點(diǎn)為</navigation>

Navigator 抽象類敦第。NavController中的navigation()會(huì)轉(zhuǎn)到它的子類峰弹,包括NavGraphNavigator,ActivityNavigator芜果,F(xiàn)ragmentNavigator鞠呈,DialogFragmentNavigator。他們會(huì)重寫Navigator的navigation()方法右钾,實(shí)現(xiàn)自己的跳轉(zhuǎn)邏輯

NavigatorProvider: 是各種Navigator的管理者蚁吝,想要定義自己的Navigator旱爆,就必須想這個(gè)類里的map進(jìn)行注冊(cè)

源碼分析

理解上面類的作用,我們從容器開始入手窘茁,看NavHostFragment怀伦,是如何獲取xml中配置的屬性:

app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true"
android:name="androidx.navigation.fragment.NavHostFragment"

xml中的屬性,通常在AttributeSet attrs中獲取山林,但Fragment的構(gòu)造函數(shù)顯然不會(huì)有此屬性房待。但我們?cè)诖祟愔邪l(fā)現(xiàn)下面的函數(shù):

  @CallSuper
    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
            @Nullable Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);

        
        final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
                //1
        final int graphId = navHost.getResourceId(
                androidx.navigation.R.styleable.NavHost_navGraph, 0);
        if (graphId != 0) {
           //2
            mGraphId = graphId;
        }
        navHost.recycle();

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
        //3
        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
        if (defaultHost) {
        //4
            mDefaultNavHost = true;
        }
        a.recycle();
    }

View(ViewGroup),fragment等在可以在XML中定義的標(biāo)簽捌朴,在繪制結(jié)束后吴攒,會(huì)執(zhí)行onInflate()方法。通過1.處解析砂蔽,得到nav_graph資源id洼怔,并保存在mGraphId變量;3.處解析獲取 app:defaultNavHost="true"設(shè)置的參數(shù)左驾。

接下來看OnCreate()方法:

        //1
        mNavController = new NavHostController(context);
        mNavController.setLifecycleOwner(this);
        //2
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        // Set the default state - this will be updated whenever
        // onPrimaryNavigationFragmentChanged() is called
        mNavController.enableOnBackPressed(
                mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
        mIsPrimaryBeforeOnCreate = null;
        mNavController.setViewModelStore(getViewModelStore());
        //3
        onCreateNavController(mNavController);

        
        if (mGraphId != 0) {//4
            // Set from onInflate()
            
            mNavController.setGraph(mGraphId);
        } else {
            // See if it was set by NavHostFragment.create()
            final Bundle args = getArguments();
            final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
            final Bundle startDestinationArgs = args != null
                    ? args.getBundle(KEY_START_DESTINATION_ARGS)
                    : null;
            if (graphId != 0) {
                mNavController.setGraph(graphId, startDestinationArgs);
            }
        }

核心代碼如上镣隶,接下里是分步走分析

1. mNavController = new NavHostController(context);

跟蹤:

 public NavHostController(@NonNull Context context) {
        super(context);
    }

NavHostController這個(gè)類沒有任何邏輯,什么都沒做诡右,目的就是為了和NavHostFragment在形式上統(tǒng)一安岂,直接去看父類: super(context);

public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }

mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider))

mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));

剛開始就在Navigator管理器NavigatorProvider這個(gè)添加NavGraphNavigator和ActivityNavigator跳轉(zhuǎn)類。這樣做的理由是帆吻,Navigation框架作為路由導(dǎo)航域那,可以不用Fragment和Dialog,但不能沒有啟動(dòng)頁(yè)和Activity路由跳轉(zhuǎn)類猜煮。換句話說幔亥,我們的App可以沒有Fragment和Dialog胯舷。但不能沒有Activity柑潦。而Navigation框架不允許沒有啟動(dòng)首頁(yè)件炉,所以必須有NavGraphNavigator這個(gè)啟動(dòng)首頁(yè)的跳轉(zhuǎn)路由類。

繼續(xù)跟蹤:mNavigatorProvider.addNavigator(new ActivityNavigator(mContext))

NavigatorProvider:

   @Nullable
    public final Navigator<? extends NavDestination> addNavigator(
            @NonNull Navigator<? extends NavDestination> navigator) {
            //name為 activity 
        String name = getNameForNavigator(navigator.getClass());
        //1.下面會(huì)分析這里愕撰,記得回頭看
        return addNavigator(name, navigator);
    }
    
     @NonNull
    static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
        String name = sAnnotationNames.get(navigatorClass);
        if (name == null) {
        // annotation此時(shí)為:activity/fragment/dialog,navigation
        //2. 下面會(huì)分析這里刹衫,記住這個(gè)位置
            Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
            name = annotation != null ? annotation.value() : null;
            if (!validateName(name)) {
                throw new IllegalArgumentException("No @Navigator.Name annotation found for "
                        + navigatorClass.getSimpleName());
            }
            //3. 下面會(huì)分析這里,記得回頭看
            sAnnotationNames.put(navigatorClass, name);
        }
        return name;
    }

Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);

Navigator.Name 是個(gè)注解類搞挣,他會(huì)用在所有Navigator所有子類的類頭带迟,用來標(biāo)記 子類是什么類型的Navigator,如下:

Activity:

@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination>

Dialog:

@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination>

Navigation:
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph

Fragment:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination>


恰好對(duì)應(yīng)nav_graph.xml中4中Destination標(biāo)簽囱桨,側(cè)面驗(yàn)證他們都具有各自的navigation()跳轉(zhuǎn)邏輯邮旷。

繼續(xù)回到 getNameForNavigator()方法。
2. annotation此時(shí)為:activity/fragment/dialog,navigation

3. sAnnotationNames.put(navigatorClass, name);存放的格式為put(ActivityNavigator.class,activity)

1. 回到上面的1.位置:

 public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
            @NonNull Navigator<? extends NavDestination> navigator) {
        if (!validateName(name)) {
            throw new IllegalArgumentException("navigator name cannot be an empty string");
        }
        return mNavigators.put(name, navigator);
    }
    

mNavigators為HashMap<String, Navigator<? extends NavDestination>> mNavigators 此類中最核心的管理類蝇摸,添加進(jìn)去存放的格式為put(activity婶肩,ActivityNavigator.class)
同時(shí) 返回Navigator對(duì)應(yīng)的NavDestination 很重要

以上第一部分完成∶蚕Γ總結(jié)如下:

  1. NavHostController 這個(gè)類沒啥實(shí)際作用律歼,就是為了和NavHostFragment形式上同樣,真正的實(shí)現(xiàn)都在父類NavController中
  2. 想要自定義自己的Navigator啡专,必須繼承Navigator险毁,并且在類頭上定義自己的Navigator.Name。
  3. 自定義的Navigator们童,必須加入NavigatorProvider#mNavigators這個(gè)Map中注冊(cè)Navigator.Name的value就是Map的key

深入的有點(diǎn)深畔况,此時(shí)回頭看往上看OnCreate()方法2處

2.mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

此方法是實(shí)現(xiàn)fragment回退的關(guān)鍵,requireActivity().getOnBackPressedDispatcher()這個(gè)是Activity返回鍵監(jiān)聽的分發(fā)器OnBackPressedDispatcher慧库。

NavController:

void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
        if (mLifecycleOwner == null) {
            throw new IllegalStateException("You must call setLifecycleOwner() before calling "
                    + "setOnBackPressedDispatcher()");
        }
        // Remove the callback from any previous dispatcher
        mOnBackPressedCallback.remove();
        // Then add it to the new dispatcher
        dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);

...略
    }

dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);將獲得的OnBackPressedDispatcher對(duì)象傳入,并向里面注冊(cè)監(jiān)聽OnBackPressedDispatcher

OnBackPressedDispatcher:

   private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            popBackStack();
        }
    };

注冊(cè)監(jiān)聽后吵瞻,當(dāng)dispatcher分發(fā)返回鍵點(diǎn)擊事件時(shí)甘磨,會(huì)回調(diào)我們注冊(cè)的監(jiān)聽济舆,從而調(diào)用popBackStack(); 出棧方法

總結(jié):

  1. 給我們個(gè)提示,如果我們有需求要攔截返回鍵签夭,做我們想做的事情椎瘟,可以像dispatcher注冊(cè)我們自己的監(jiān)聽回調(diào)。

此時(shí)回頭看往上看OnCreate()方法3處

3. onCreateNavController(mNavController);

跟蹤:

   protected void onCreateNavController(@NonNull NavController navController) {
        navController.getNavigatorProvider().addNavigator(
                new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
        navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }
    
     protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
        return new FragmentNavigator(requireContext(), getChildFragmentManager(),
                getContainerId());
    }

添加DialogFragmentNavigator和FragmentNavigator跳轉(zhuǎn)支持肺蔚,此時(shí)4種Navigator煌妈,全部添加進(jìn)NavigationProvider的HashMap中。支持4中標(biāo)跳轉(zhuǎn)的能力

總結(jié):

  1. Navigation路由跳轉(zhuǎn)的容器必須是NavHostFragment,否則無法支持Dialog和Fragment跳轉(zhuǎn)能力
  2. 如果自定義FragmentNavigator和DialogFragmentNavigator類型宣羊,傳入的FragmentManagergetChildFragmentManager()

此時(shí)回頭看往上看OnCreate()方法4處:

4. onCreateNavController(mNavController);


 if (mGraphId != 0) {//4
            // Set from onInflate()
            
            mNavController.setGraph(mGraphId);
        } else {
            // See if it was set by NavHostFragment.create()
            final Bundle args = getArguments();
            final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
            final Bundle startDestinationArgs = args != null
                    ? args.getBundle(KEY_START_DESTINATION_ARGS)
                    : null;
            if (graphId != 0) {
                mNavController.setGraph(graphId, startDestinationArgs);
            }
        }

無論走哪個(gè)分支璧诵,必然調(diào)用mNavController.setGraph()方法:

在這里暫停,下面跟隨代碼深入仇冯,會(huì)越來越深之宿,但思路清晰,暫且在這里設(shè)置個(gè)錨點(diǎn)1苛坚,會(huì)說回到錨點(diǎn)1 就是setGraph()這個(gè)方法

錨點(diǎn)1

  @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }

getNavInflater():

 public NavInflater getNavInflater() {
        if (mInflater == null) {
            mInflater = new NavInflater(mContext, mNavigatorProvider);
        }
        return mInflater;
    }
    

創(chuàng)建:mInflater比被,接著進(jìn)入mInflater#inflate():

 @SuppressLint("ResourceType")
    @NonNull
    public NavGraph inflate(@NavigationRes int graphResId) {
        Resources res = mContext.getResources();
        XmlResourceParser parser = res.getXml(graphResId);
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }

            String rootElement = parser.getName();
            // 1 
            NavDestination destination = inflate(res, parser, attrs, graphResId);
            if (!(destination instanceof NavGraph)) {
                throw new IllegalArgumentException("Root element <" + rootElement + ">"
                        + " did not inflate into a NavGraph");
            }
            2.
            return (NavGraph) destination;
        } catch (Exception e) {
            throw new RuntimeException("Exception inflating "
                    + res.getResourceName(graphResId) + " line "
                    + parser.getLineNumber(), e);
        } finally {
            parser.close();
        }
    }

這個(gè)方法的返回值為NavGraph色难,這點(diǎn)要記住。也就是1處返回的對(duì)象destination等缀,實(shí)際是NavGraph,所以在2處強(qiáng)轉(zhuǎn)返回.跟進(jìn)1處代碼:

錨點(diǎn)2

  private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
            @NonNull AttributeSet attrs, int graphResId)
            throws XmlPullParserException, IOException {
        //1   
        Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
        //2
        final NavDestination dest = navigator.createDestination();
        //3.
        dest.onInflate(mContext, attrs);

        final int innerDepth = parser.getDepth() + 1;
        int type;
        int depth;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth
                || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            if (depth > innerDepth) {
                continue;
            }

            final String name = parser.getName();
            if (TAG_ARGUMENT.equals(name)) {
                inflateArgumentForDestination(res, dest, attrs, graphResId);
            } else if (TAG_DEEP_LINK.equals(name)) {
                inflateDeepLink(res, dest, attrs);
            } else if (TAG_ACTION.equals(name)) {
                inflateAction(res, dest, attrs, parser, graphResId);
            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                final TypedArray a = res.obtainAttributes(
                        attrs, androidx.navigation.R.styleable.NavInclude);
                final int id = a.getResourceId(
                        androidx.navigation.R.styleable.NavInclude_graph, 0);
                        //4
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
            //5
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }
        }

        return dest;
    }

這個(gè)方法很重要枷莉。首先我們知道NavGraph中是包含nav_graph所有節(jié)點(diǎn)的內(nèi)容。所以進(jìn)入方法時(shí)是<navigation>這個(gè)根節(jié)點(diǎn)標(biāo)簽尺迂,1.中navigator=NavGraphNavigator 2.navigator.createDestination()就是dest=new NavGraph(this) this=NavGraphNavigator:

跟進(jìn):

 public NavGraph createDestination() {
        return new NavGraph(this);
    }
    
     public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) {
        super(navGraphNavigator);
    }
      public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
        this(NavigatorProvider.getNameForNavigator(navigator.getClass()));
    }

3笤妙,中調(diào)用的onInflate()則為NavGraph類中的

NavGraph:
@Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
        super.onInflate(context, attrs);
        TypedArray a = context.getResources().obtainAttributes(attrs,
                R.styleable.NavGraphNavigator);
        setStartDestination(
                a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
        mStartDestIdName = getDisplayName(context, mStartDestId);
        a.recycle();
    }
    
     public final void setStartDestination(@IdRes int startDestId) {
        if (startDestId == getId()) {
            throw new IllegalArgumentException("Start destination " + startDestId + " cannot use "
                    + "the same id as the graph " + this);
        }
        mStartDestId = startDestId;
        mStartDestIdName = null;
    }

setStartDestination()根據(jù)startDestination就是我們?cè)趎av_graph的根節(jié)點(diǎn)設(shè)置的app:startDestination="@+id/navigation_home"參數(shù),賦值mStartDestId和mStartDestIdName噪裕,這里我們知道蹲盘,當(dāng)<navigation>嵌套時(shí),不能使用相同的app:startDestination="@+id/navigation_home"ID

繼續(xù)回到4

解析完根節(jié)點(diǎn)后薄嫡,會(huì)在循環(huán)中,進(jìn)入到4或5?哑蔫,然后遞歸調(diào)用闸迷。遞歸中dest分別可能為Navigator的另外三個(gè)子類ActivityNavigator,DialogFragmentNavigator今阳,F(xiàn)ragmentNavigator
然后分別調(diào)用他們的navigator.createDestination()方法。然后分別調(diào)用:dest.onInflate(mContext, attrs)

逐一解析:ActivityNavigator#Destination#onInflate( Context context, AttributeSet attrs)

@CallSuper
        @Override
        public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.ActivityNavigator);
            String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
            if (targetPackage != null) {
                targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
                        context.getPackageName());
            }
            //1
            setTargetPackage(targetPackage);
            
            String className = a.getString(R.styleable.ActivityNavigator_android_name);
            if (className != null) {
                if (className.charAt(0) == '.') {
                //2
                    className = context.getPackageName() + className;
                }
                //3
                setComponentName(new ComponentName(context, className));
            }
            //4
            setAction(a.getString(R.styleable.ActivityNavigator_action));
            String data = a.getString(R.styleable.ActivityNavigator_data);
            if (data != null) {
            //5
                setData(Uri.parse(data));
            }
            setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
            a.recycle();
        }

解析<activity>標(biāo)簽1.獲取包名 2.得到全類名 3.4.5進(jìn)入方法內(nèi)部,都是對(duì)Intent進(jìn)行賦值膝舅。我們知道鼻疮,設(shè)置ComponentName,我們就可以進(jìn)行最基本的跳轉(zhuǎn)崭篡,如下:

 public final Destination setComponentName(@Nullable ComponentName name) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setComponent(name);
            return this;
        }

而且從xml代碼中可以看出,id颠毙,name是必要參數(shù),有他們2就可以路由跳轉(zhuǎn)

    <activity
        android:id="@+id/navigation_home"
        android:name="org.devio.proj.navigationpro.NavigationActivity"
        android:label="@string/title_home"
        tools:layout="@layout/activity_main" />

結(jié)論:onInflate()方法的核心是setComponentName(),也就是說當(dāng)我們自定義Navigatior時(shí)滴某,要配置setComponentName()

逐一解析:FragmentNavigator#Destination#onInflate( Context context, AttributeSet attrs)

 public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.FragmentNavigator);
            String className = a.getString(R.styleable.FragmentNavigator_android_name);
            if (className != null) {
                setClassName(className);
            }
            a.recycle();
        }

這個(gè)方法就很簡(jiǎn)單, setClassName(className); 保存Fragment的全類名

逐一解析:DialogFragmentNavigator#Destination#onInflate( Context context, AttributeSet attrs)

   public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.DialogFragmentNavigator);
            String className = a.getString(R.styleable.DialogFragmentNavigator_android_name);
            if (className != null) {
                setClassName(className);
            }
            a.recycle();
        }

也是如此

回到錨點(diǎn)2


   private static final String TAG_ARGUMENT = "argument";1
    private static final String TAG_DEEP_LINK = "deepLink";2
    private static final String TAG_ACTION = "action";3
    private static final String TAG_INCLUDE = "include";4


 final String name = parser.getName();
            if (TAG_ARGUMENT.equals(name)) {
                inflateArgumentForDestination(res, dest, attrs, graphResId);
            } else if (TAG_DEEP_LINK.equals(name)) {
                inflateDeepLink(res, dest, attrs);
            } else if (TAG_ACTION.equals(name)) {
                inflateAction(res, dest, attrs, parser, graphResId);
            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                final TypedArray a = res.obtainAttributes(
                        attrs, androidx.navigation.R.styleable.NavInclude);
                final int id = a.getResourceId(
                        androidx.navigation.R.styleable.NavInclude_graph, 0);
                        //
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
            //
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }

通過對(duì)1,2晤硕,3,4子標(biāo)簽的解析创译,然后存到對(duì)應(yīng)的NavDestination中软族,掖疮。同時(shí)在遞歸中把所有NavDestination節(jié)點(diǎn)addDestination()到NavGraph中:

NavGraph:

SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
    public final void addDestination(@NonNull NavDestination node) {
        ...略
        mNodes.put(node.getId(), node);
    }

總結(jié): 所有NavDestination都要存入mNodes

回到錨點(diǎn)1

   private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
       ...略
        if (mGraph != null && mBackStack.isEmpty()) {
            boolean deepLinked = !mDeepLinkHandled && mActivity != null
                    && handleDeepLink(mActivity.getIntent());
            if (!deepLinked) {
                // Navigate to the first destination in the graph
                // if we haven't deep linked to a destination
                //開始真正的跳轉(zhuǎn)
                navigate(mGraph, startDestinationArgs, null, null);
            }
        } else {
            dispatchOnDestinationChanged();
        }
    }

navigate(mGraph, startDestinationArgs, null, null); startDestinationArgs,意味著跳轉(zhuǎn)路由搁宾,啟動(dòng)第一個(gè)app:startDestination="@+id/navigation_home" 配置的節(jié)點(diǎn)頁(yè)面

navigate()

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        //3
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            
         
         ...略
         
         //1
            // The mGraph should always be on the back stack after you navigate()
            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.addFirst(entry);
            }
            //2
            // And finally, add the new destination with its default args
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            launchSingleTop = true;
            NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
            if (singleTopBackStackEntry != null) {
                singleTopBackStackEntry.replaceArguments(finalArgs);
            }
        }
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null || launchSingleTop) {
            dispatchOnDestinationChanged();
        }
    }
  1. mGraph必須在導(dǎo)航后加入回退棧mBackStack,如果回退棧為空翩腐,那么mGraph一定是第一個(gè)添加的元素
  2. 把新的目標(biāo)NavDestination也加入進(jìn)回退棧

經(jīng)過這里,導(dǎo)航具有了返回棧的能力疙筹。

繼續(xù)看3.
根據(jù)參數(shù)得知而咆,navigator為NavGraphNavigator。

NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); newDest為NavGraph涯捻。navigator.navigate,會(huì)調(diào)用到NavGraphNavigator中:

            跟蹤到下面方法:    
            
            ```
            
            NavGraphNavigator:
            
            
             @Nullable
@Override
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
    int startId = destination.getStartDestination();
    if (startId == 0) {
        throw new IllegalStateException("no start destination defined via"
                + " app:startDestination for "
                + destination.getDisplayName());
    }
    //startDestination可能是ActivityNavigator DialogFragmentNavigator FragmentNavigator NavGraphNavigator
    NavDestination startDestination = destination.findNode(startId, false);
    if (startDestination == null) {
        final String dest = destination.getStartDestDisplayName();
        throw new IllegalArgumentException("navigation destination " + dest
                + " is not a direct child of this NavGraph");
    }
    Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
            startDestination.getNavigatorName());
    return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
            navOptions, navigatorExtras);
}

            ```

int startId = destination.getStartDestination(); 找到節(jié)點(diǎn)的startId,如果沒配置涛浙。拋出異常疮薇。找到id對(duì)應(yīng)的節(jié)點(diǎn),找不到拋出異常但骨。
此時(shí)設(shè)置的首頁(yè)節(jié)點(diǎn)可能是<activity><fragment><dialog>avigator.navigate()然后繼續(xù)開始導(dǎo)航

總結(jié):

  1. 如果沒配置startId呀伙。拋出異常
  2. 找不到對(duì)應(yīng)的NavDestination箫锤,拋出異常

上面就是啟動(dòng)首頁(yè)第一個(gè)頁(yè)面的導(dǎo)航路由過程阳准,下面路由分到<activity><fragment><dialog>中

ActivityNavigator#navigate()

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (destination.getIntent() == null) {
            throw new IllegalStateException("Destination " + destination.getId()
                    + " does not have an Intent set.");
        }
        //1
        Intent intent = new Intent(destination.getIntent());
        if (args != null) {
            intent.putExtras(args);
            String dataPattern = destination.getDataPattern();
            if (!TextUtils.isEmpty(dataPattern)) {
                // Fill in the data pattern with the args to build a valid URI
                StringBuffer data = new StringBuffer();
                Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
                Matcher matcher = fillInPattern.matcher(dataPattern);
                while (matcher.find()) {
                    String argName = matcher.group(1);
                    if (args.containsKey(argName)) {
                        matcher.appendReplacement(data, "");
                        //noinspection ConstantConditions
                        data.append(Uri.encode(args.get(argName).toString()));
                    } else {
                        throw new IllegalArgumentException("Could not find " + argName + " in "
                                + args + " to fill data pattern " + dataPattern);
                    }
                }
                matcher.appendTail(data);
                intent.setData(Uri.parse(data.toString()));
            }
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            intent.addFlags(extras.getFlags());
        }
        
        //2
        if (!(mContext instanceof Activity)) {
            // If we're not launching from an Activity context we have to launch in a new task.
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        }
        if (mHostActivity != null) {
            final Intent hostIntent = mHostActivity.getIntent();
            if (hostIntent != null) {
                final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
                if (hostCurrentId != 0) {
                    intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
                }
            }
        }
        //3
        final int destId = destination.getId();
        intent.putExtra(EXTRA_NAV_CURRENT, destId);
        Resources resources = getContext().getResources();
        if (navOptions != null) {
            int popEnterAnim = navOptions.getPopEnterAnim();
            int popExitAnim = navOptions.getPopExitAnim();
            if ((popEnterAnim > 0
                    && resources.getResourceTypeName(popEnterAnim).equals("animator"))
                    || (popExitAnim > 0
                    && resources.getResourceTypeName(popExitAnim).equals("animator"))) {
                Log.w(LOG_TAG, "Activity destinations do not support Animator resource. Ignoring "
                        + "popEnter resource " + resources.getResourceName(popEnterAnim) + " and "
                        + "popExit resource " + resources.getResourceName(popExitAnim) + "when "
                        + "launching " + destination);
            } else {
                // For use in applyPopAnimationsToPendingTransition()
                intent.putExtra(EXTRA_POP_ENTER_ANIM, popEnterAnim);
                intent.putExtra(EXTRA_POP_EXIT_ANIM, popExitAnim);
            }
        }
        //4
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            ActivityOptionsCompat activityOptions = extras.getActivityOptions();
            if (activityOptions != null) {
                ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
            } else {
                mContext.startActivity(intent);
            }
        } else {
            mContext.startActivity(intent);
        }
        if (navOptions != null && mHostActivity != null) {
            int enterAnim = navOptions.getEnterAnim();
            int exitAnim = navOptions.getExitAnim();
            if ((enterAnim > 0 && resources.getResourceTypeName(enterAnim).equals("animator"))
                    || (exitAnim > 0
                    && resources.getResourceTypeName(exitAnim).equals("animator"))) {
                    Log.w(LOG_TAG, "Activity destinations do not support Animator resource. "
                            + "Ignoring " + "enter resource " + resources.getResourceName(enterAnim)
                            + " and exit resource " + resources.getResourceName(exitAnim) + "when "
                            + "launching " + destination);
            } else if (enterAnim >= 0 || exitAnim >= 0) {
                enterAnim = Math.max(enterAnim, 0);
                exitAnim = Math.max(exitAnim, 0);
                mHostActivity.overridePendingTransition(enterAnim, exitAnim);
            }
        }

        // You can't pop the back stack from the caller of a new Activity,
        // so we don't add this navigator to the controller's back stack
        return null;
    }
  1. 創(chuàng)建跳轉(zhuǎn)Intent
  2. 如果不是通過mContext啟動(dòng)(其他進(jìn)程或應(yīng)用,例如deeplink)設(shè)置FLAG_ACTIVITY_NEW_TASK 設(shè)置xml中app:launchSingleTop="true"則乍狐,F(xiàn)LAG_ACTIVITY_SINGLE_TOP
  3. 設(shè)置跳轉(zhuǎn)動(dòng)畫
  4. startActivity(intent)跳轉(zhuǎn)

FragmentNavigator#navigate()

@Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        //1
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
//2
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
//3
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                mFragmentManager.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        //4
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }
    
    
    
    @Deprecated
    @NonNull
    public Fragment instantiateFragment(@NonNull Context context,
            @NonNull FragmentManager fragmentManager,
            @NonNull String className, @SuppressWarnings("unused") @Nullable Bundle args) {
        return fragmentManager.getFragmentFactory().instantiate(
                context.getClassLoader(), className);
    }
  1. 利用全類名惜傲,調(diào)用instantiateFragment()反射獲得Fragment實(shí)例
  2. 設(shè)置動(dòng)畫效果
  3. 創(chuàng)建FragmentTransaction事務(wù),用ft.replace展示
  4. 提交

ft.replace()會(huì)重建View(驗(yàn)證onDestroyView->onCreateView,并不會(huì)走到onDestroy),保留實(shí)體。
如果我們想以吻贿,shou舅列,hide方式該怎么做?

DialogFragmentNavigator#navigate()


DialogFragmentNavigator


  @Nullable
    @Override
    public NavDestination navigate(@NonNull final Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        final Fragment frag = mFragmentManager.getFragmentFactory().instantiate(
                mContext.getClassLoader(), className);
        if (!DialogFragment.class.isAssignableFrom(frag.getClass())) {
            throw new IllegalArgumentException("Dialog destination " + destination.getClassName()
                    + " is not an instance of DialogFragment");
        }
        final DialogFragment dialogFragment = (DialogFragment) frag;
        dialogFragment.setArguments(args);
        dialogFragment.getLifecycle().addObserver(mObserver);

        dialogFragment.show(mFragmentManager, DIALOG_TAG + mDialogCount++);

        return destination;
    }

dialogFragment.show()he dismiss()顯示隱藏,

整體流程和源碼分析結(jié)束赠橙∑诰荆總結(jié)一下幾點(diǎn):

  1. Navigator.Name 是個(gè)注解類,他會(huì)用在所有Navigator所有子類的類頭缤苫,用來標(biāo)記 子類是什么類型的Navigator
  2. 自定義自己的Navigator,必須繼承Navigator帜矾,并且在類頭上定義自己的Navigator.Name
  3. 自定義的Navigator珍剑,必須加入NavigatorProvider#mNavigators這個(gè)Map中注冊(cè)Navigator.Name的value就是Map的key
  4. NavHostController 這個(gè)類沒啥實(shí)際作用招拙,就是為了和NavHostFragment形式上同樣饰序,真正的實(shí)現(xiàn)都在父類NavController中
  5. 有需求要攔截返回鍵,做我們想做的事情诉稍,可以像dispatcher注冊(cè)我們自己的監(jiān)聽回調(diào)蚤告。
  6. Navigation路由跳轉(zhuǎn)的容器必須是NavHostFragment,否則無法支持Dialog和Fragment跳轉(zhuǎn)能力
  7. 如果自定義FragmentNavigator和DialogFragmentNavigator類型,傳入的FragmentManagergetChildFragmentManager()
  8. 所有NavDestination都要存入NavGraph#mNodes
  9. 如果沒配置startId箫章。拋出異常,找不到對(duì)應(yīng)的NavDestination戳表,拋出異常

Navigation 優(yōu)缺點(diǎn)

優(yōu)點(diǎn):
支持Activity匾旭,F(xiàn)ragment,Dialog跳轉(zhuǎn)
safesArgs安全數(shù)據(jù)傳輸
允許自定義導(dǎo)航行為
支持Deeplink
可視化編輯頁(yè)面
回退棧管理
Android組件(如:BottomNavigationView)完美交互,JetPack其他組件聯(lián)合使用

缺點(diǎn):
所有節(jié)點(diǎn)定義在nav_graph.xml不方便管理居兆,靈活性較差
Fragment切換時(shí)用replace()銷貨視圖泥栖,重新綁定數(shù)據(jù)

下篇將對(duì)Navigation進(jìn)行實(shí)戰(zhàn)改造去除店xml文件,利用json文件+注解形式钞它,動(dòng)態(tài)生成路由文件和管理路由配置

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市甸陌,隨后出現(xiàn)的幾起案子须揣,更是在濱河造成了極大的恐慌,老刑警劉巖钱豁,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耻卡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡牲尺,警方通過查閱死者的電腦和手機(jī)卵酪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門搓茬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粘昨,“玉大人捌浩,你說我怎么就攤上這事螟碎。” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我仑最,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任治唤,我火速辦了婚禮疙挺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘樟氢。我一直安慰自己毅该,他們只是感情好召噩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著萎攒,像睡著了一般喧锦。 火紅的嫁衣襯著肌膚如雪宿稀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音道媚,去河邊找鬼。 笑死育勺,一個(gè)胖子當(dāng)著我的面吹牛哑了,可吹牛的內(nèi)容都是我干的倒信。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狈谊,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤喜命,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后河劝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壁榕,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年赎瞎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牌里。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡务甥,死狀恐怖牡辽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情敞临,我是刑警寧澤态辛,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站挺尿,受9級(jí)特大地震影響奏黑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜编矾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一熟史、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧窄俏,春花似錦以故、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至踪区,卻和暖如春昆烁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缎岗。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工静尼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓鼠渺,卻偏偏與公主長(zhǎng)得像鸭巴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拦盹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容