牛逼!像使用Activity一样使用Fragment

刘望舒

共 35442字,需浏览 71分钟

 ·

2021-05-18 23:06

 微信改了推动机制,真爱请星标本公号
公众号回复加入BATcoder技术群BAT

作者:fundroid_方卓 

https://blog.csdn.net/vitaviva

前言

" An app only needs an Activity, you can use Fragments, just don’t use the backstack with fragments "
– Jake Wharton @Droidcon NYC 2017

近年来,SPA,即单Activity架构逐渐开始受到欢迎,随之而生了很多优秀的三方库,大部分是基于Fragment作为实现方案,其中最有代表性的就是Fragmentation了,后来Jetpack Navigation的诞生也标志着Google从官方立场对SPA架构的肯定。

Navigation的出现并没有加速Fragment对Activity的全面取代,一个重要原因是因为其过与依赖配置(NavGraph),丧失了Activity的灵活性。这一点上Fragmentation做的不错,有接近Activity的使用体验,可惜其不支持Kotlin,且早已停止维护,无法使用近年来在AndroidX中引入的各种新特性。

是否有一个工具,既具备Fragmentation那样灵活性,又能像Navigation那样兼容AndroidX中的新功能呢?Fragivity正是在这个背景下诞生的。

Fragivity的项目地址:
https://github.com/vitaviva/fragivity

Fragmentation的项目地址:
https://github.com/YoKeyword/Fragmentation

Use Fragment Like Activity

顾名思义,Fragivity希望让Fragment具备Activity一样的使用体验,从而在各种场景中能真正取而代之:

  • 生命周期与Activity行为一致
  • 支持多种LaunchMode
  • 支持OnBackPressed事件处理、支持SwipeBack
  • 支持Transition、SharedElement等转场动画
  • 支持以Dialog样式显示
  • 支持Deep Links

Fragivity底层基于Navigation实现,同时兼具Fragmentation的灵活性,无需配置NavGraph即可实现画面跳转。简单对比一下三者的区别:


Fragmentation
Navigation
Fragivity
自由跳转
yes
no (依赖NavGraph)
yes
Launch Mode
3种
2种
3种
支持Deep Links
no
yes(依赖NavGraph)
yes(使用注解)
kotlin友好
no
yes
yes
生命周期
与Activity不一致(add方式)
与Activity不一致(replace方式)
与Activity一致
Fragment间通信
startFragmentForResult
viewmodel
viewmodel、callback、resultapi等多种方式
过场动画
View Animation
Transition Animation
Transition Animation
Swipe Back
yes(依赖基类)
no
yes (无需基类)
支持Dialog显示
no
yes
yes
OnBackPressed拦截
yes (依赖基类)
yes(无需基类)
yes(无需基类)


通过对比可以发现,比起前两者Fragivity在多个维度上与Activity的行为更加一致。

基本使用

Fragivity的接入成本很低。

gradle依赖


implementation 'com.github.fragivity:core:$latest_version'


声明NavHostFragment


像Navigation一样,Fragivity需要NavHostFragment作为Parent,然后在ChildFragment之间实现页面跳转。我们在xml中声明NavHostFragment:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:fitsSystemWindows="true">


    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

加载首页

通常我们需要定义一个MainActivity作为入口,同样,这里通过loadRoot加载一个初始的Fragment:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.loadRoot(HomeFragment::class)

    }
}


页面跳转


接下来便可以在Fragment之间进行跳转了:

//跳转到目标Fragment
navigator.push(DestinationFragment::class)

//携带参数跳转
val bundle = bundleOf(KEY_ARGUMENT to "some args")
navigator.push(DestinationFragment::classbundle)

 

页面返回


通过pop方法可以返回上一页面。

//返回上一页面
navigator.pop()

//返回到指定页面
navigator.popTo(HomeFramgent::class)

转场动画


基于Navigation的能力,在画面跳转时可以设置Transition动画。

navigator.push(UserProfile::classbundle) { //this:NavOptions
    //配置动画
    enterAnim = R.anim.enter_anim
    exitAnim = R.anim.exit_anim
    popEnterAnim = R.anim.enter_anim
    popExitAnim = R.anim.exit_anim
}

借助FragmentNavigatorExtras还可以设置SharedElement,实现更优雅地动画效果。

//跳转时,对imageView设置SharedElement
navigator.push(UserProfile::classbundle,
               FragmentNavigatorExtras
(imageView to "imageView")) { //this:NavOptions

    enterAnim = R.anim.enter_anim
    exitAnim = R.anim.exit_anim
    popEnterAnim = R.anim.enter_anim
    popExitAnim = R.anim.exit_anim

}

class UserProfile : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //目标Fragment中设置共享元素动画
        sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
    }

}

无需配置实现页面跳转

Navigation需要配置NavGraph才能实现页面间跳转,例如:

<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@+id/first">


    <fragment
        android:id="@+id/fragment_first"
        android:name=".FirstFagment"
        android:label="@string/tag_first">

        <action
            android:id="@+id/action_to_second"
            app:destination="@id/fragment_second"/>

    </fragment>
    <fragment
        android:id="@+id/fragment_second"
        android:name=".SecondFragment"
        android:label="@string/tag_second"/>

</navigation>

每个<navigation/>对应一个NavGraph对象,<fragment/>会对应到NavGraph中的各个Destination,NavController持有NavGraph通过控制Destination之间的跳转。

依赖配置的页面跳转,无法做到像Activity那样灵活。Fragivity通过动态构建NavGraph,无需配置即可实现跳转:

动态创建Graph


加载首页时,动态创建Graph。

fun NavHostFragment.loadRoot(root: KClass<out Fragment>) {

    navController.apply {
        //添加Navigator
        navigatorProvider.addNavigator(
            FragivityNavigator(
                context,
                childFragmentManager,
                id
            )
        )

        //创建Graph
        graph = createGraph(startDestination = startDestId) {
            val startDestId = root.hashCode()
            //添加startDestination
            destination(
                FragmentNavigatorDestinationBuilder(
                    provider[FragivityNavigator::class],
                    startDestId,
                    root

                ))
        }
    }
}

FragivityNavigator负责处理页面跳转的逻辑,后文会单独介绍。Graph创建后添加startDestination用来加载首页。

动态添加Destination


除startDestination以外,每当跳转到新页面,都需要为Graph动态添加此Destination:

fun NavHost.push(
    clazz: KClass<out Fragment>,
    args: Bundle? = null,
    extras: Navigator.Extras? = null,
    optionsBuilder: NavOptions.()
 -> Unit = {}
) = with(navController) {
    // 动态创建Destination
    val node = putFragment(clazz)
    // 调用NavController的navigate方法进行跳转
    navigate(
        node.id, args,
        convertNavOptions(clazz, NavOptions().apply(optionsBuilder)),
        extras
    )
}

// 创建并添加Destination
private fun NavController.putFragment(clazz: KClass<out Fragment>): FragmentNavigator.Destination {
    val destId = clazz.hashCode()
    lateinit var destination: FragmentNavigator.Destination
    if (graph.findNode(destId) == null) {
        destination = (FragmentNavigatorDestinationBuilder(
            navigatorProvider[FragivityNavigator::class],
            destId,
            clazz

        )).build()
        graph.plusAssign(destination)// 添加进Graph
    } else {
        destination = graph.findNode(destId) as FragmentNavigator.Destination
    }
    return destination
}

创建Destination后,通过NavController的navigate方法跳转到此Destination。

BackStack及生命周期

如J神所说,Fragment无法很好替代Activity的原因之一是在BackStack管理上的差异,这会影响到生命周期的不同。

设想以下场景:A页面 > (启动)> B页面 >(返回)> A页面


我们知道添加Fragment一般有两种方式:add 、 replace。无论哪种方式其在画面跳转时的生命周期与Activity都不相同:

页面B的启动方式
从B返回时的生命周期
Activity
ActivityB:onPasue -> onStop -> onDestroy
ActivityA:onStart -> onResume
Fragment(add )
FragmentB : onPause -> onStop -> onDestroy
FragmentA : no change
Fragment(replace)
FragmentB: onPause -> onStop -> onDestroy
FragmentA: onCreateView -> onStart -> onResume

如果希望在画面跳转时Fragment的生命周期与Activity行为一致,则至少需要达成以下三个目标:

  • 目标1:回退时,FragmentB不重新onCreateView (add方式满足)
  • 目标2:回退时,FragmentB会触发onStart -> onResume (replace方式满足)
  • 目标3:后台的Fragment不跟随父生命周期发生变化 (replace方式满足)


无论Navigation还是Fragmentation都不能同时满足上面三条。


重写FragmentNavigator


NavController通过FragmentNavigator实现具体的跳转逻辑,FragmentNavigator是Navigator的派生类,专门负责FragmentNavigator.Destination类型的跳转。

navigate()实现了Fragment跳转的具体逻辑,其核心代码如下:

@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination{

    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

        String className = destination.getClassName();

        //实例化Fragment
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
        className, args);
        frag.setArguments(args);

        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        ft.replace(mContainerId, frag); // replace方式添加Fragment
        ft.setPrimaryNavigationFragment(frag);

        //事务压栈
        ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));

        ft.setReorderingAllowed(true);
        ft.commit();

    }

}

FragmentNavigator通过replace进行Fragment跳转,前面分析我们知道这在回退时会重新onCreateView,不符合预期。我们实现子类FragivityNavigator,重写navigate()方法,将replace改为add,避免重新onCreateView,达成“目标1”。

public class FragivityNavigator extends FragmentNavigator {
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
        className, args);
        //ft.replace(mContainerId, frag); // replace改为add
        ft.add(mContainerId, frag, generateBackStackName(mBackStack.size(), destination.getId()));

    }
}

添加OnBackStackChangedListener


在合适的时机为FragmentManger添加OnBackStackChangedListener,当监听到backstack变化时,手动触发生命周期回调,达成“目标2”。

private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
        new FragmentManager.OnBackStackChangedListener() {

            @Override
            public void onBackStackChanged() {
                if (mIsPendingAddToBackStackOperation) {
                    mIsPendingAddToBackStackOperation = !isBackStackEqual();

                    if (mFragmentManager.getFragments().size() > 1) {
                        // 切到后台时的生命周期
                        Fragment fragment = mFragmentManager.getFragments().get(mFragmentManager.getFragments().size() - 2);
                        if (fragment instanceof ReportFragment) {
                            fragment.performPause();
                            fragment.performStop();
                            ((ReportFragment) fragment).setShow(false);
                        }
                    }
                } else if (mIsPendingPopBackStackOperation) {
                    mIsPendingPopBackStackOperation = !isBackStackEqual();
                    // 回到前台时的生命周期
                    Fragment fragment = mFragmentManager.getPrimaryNavigationFragment();
                    if (fragment instanceof ReportFragment) {
                        ((ReportFragment) fragment).setShow(true);
                        fragment.performStart();
                        fragment.performResume();
                    }
                }
            }
        };

ReportFragment代理


为了达成“目标3”, 在实例化Fragment时,为其创建ReportFragment作为代理。所谓代理其实是通过ParentFragment对内进行生命周期的分发和控制。

//ReportFragment
internal class ReportFragment : Fragment() {

    internal lateinit var className: String
    private val _real: Class<out Fragment> by lazy {
        Class.forName(className) as Class<out Fragment>
    }
    private val _realFragment by lazy {  _real.newInstance() }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        //将目标Framgent作为child进行管理
        mChildFragmentManager.beginTransaction().apply {
            _realFragment.arguments = arguments
            add(R.id.container, _realFragment)
            commitNow()
        }
    }

}

//ReportFragmentManager
internal class ReportFragmentManager : FragmentManager() {
    //isShow:在后台时,不响应生命周期分发
    internal var isShow = true
    public override fun dispatchResume() {
        if (isShow) super.dispatchResume()
    }

    //...
}

支持Launch Mode

Fragivity支持三种LaunchMode:Standard、SingleTop、SingleTask。

启动方式非常简单:

navigator.push(LaunchModeFragment::classbundle) { //this: NavOptions
    launchMode = LaunchMode.STANDARD // 默认可省略
    //launchMode = LaunchMode.SINGLE_TOP
    //launchMode = LaunchMode.SINGLE_TASK
}

这里着重介绍一下SingleTop的实现。Navigation也支持SingleTop,但是在Navigator中完成的,由于我们重写了Navigator(replace改为add),因此对SingleTop的实现也要做相应调整:

@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

    final Fragment preFrag = mFragmentManager.getPrimaryNavigationFragment();

    //当以singleTop启动时
    if (isSingleTopReplacement) {
            if (mBackStack.size() > 1) {
                ft.remove(preFrag);// 删除旧实例

                //更新FragmentTransaction中的实例信息
                frag.mTag = generateBackStackName(mBackStack.size() - 1, destination.getId());
                if (mFragmentManager.mBackStack.size() > 0) {
                    List<FragmentTransaction.Op> ops =
                            mFragmentManager.mBackStack.get(mFragmentManager.mBackStack.size() - 1).mOps;
                    for (FragmentTransaction.Op op : ops) {
                        if (op.mCmd == OP_ADD && op.mFragment == preFrag) {
                            op.mFragment = frag;
                        }
                    }
                }
            }
        } 
}

SingleTop要求当栈顶类型和目标类型相同时只能存在一个实例,所以需要删除旧实例避免重复添加。同时为了保证BackStack回退时的事务行为正常,需要将添加旧实例的事务中的相关信息更新为新实例。

Fragment通信

Fragivity支持androidx.fragment的所有通信方式,例如使用ViewModel,或者使用ResultApi(Fragment 版本高于1.3.0-beta02)等。除此之外,Fragivity提供了更简单的基于Callback的通信方式:
//SourceFragment
val cb = { it : Boolean -> 
    //...
}
navigator.push {
    DestinationFragment(cb)
}

//Destination
class DestinationFragment(val cb:(Boolean) -> Unit) {...}

以前Fragment如果必须使用无参的构造函数,否则打包时会出错。感谢AndroidX带来的进步,目前已经取消了此限制,允许自定义带参数的构造函数。因此我们可以通过lambda动态创建Fragment并将callback作为构造参数传入。
inline fun <reified T : Fragment> NavHost.push(
    noinline optionsBuilder: NavOptions.()
 -> Unit = {},
    noinline block: () -> T
) {
    //...
    push(T::classoptionsBuilder)
}

如上,其内部仍然是使用Fragment的Class作为参数进行跳转,只是借助kotlin的reified特性,获取了泛型的Class信息而已。

支持DeepLinks

Activity可以通过URI隐式启动,为了覆盖此类使用场景,需要为Fragment提供Deep Links支持。Navigation在NavGraph中为Destination配置URI信息;Fragivity虽然没有NavGraph,但可以通过注解配置URI。基本思想类似于ARouter的路由原理:

  • 在编译期通过kapt解析注解,获取URI信息,并与Fragment相关联
  • 在Activity的入口处拦截Intent,解析URI并跳转到相关联的Fragment


添加kapt依赖


kapt 'com.github.fragivity:processor:$latest_version'

配置URI


定义Fragment时,使用@DeepLink配置URI。
const val URI = "myapp://fragitiy.github.com/"

@DeepLink(uri = URI)
class DeepLinkFragment : AbsBaseFragment() {

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

处理Intent


在MainActivity入口处,处理Intent中的URI。
//MainActivity#onCreate
override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     setContentView(R.layout.activity_main)

     val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host) as NavHostFragment

     navHostFragment.handleDeepLink(intent)

}

handleDeepLink内部最终会调用NavController的相关方法对URI进行解析:

//NavController
public void navigate(@NonNull Uri deepLink) {
    navigate(new NavDeepLinkRequest(deepLink, nullnull));
}
之后,我们就可以从APP外部通过URI的方式跳转到目标Fragment了:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://fragitiy.github.com/"))
startActivity(intent)

OnBackPressed事件拦截


Fragment没有Activity的OnBackPressed方法,Fragmentation通过继承的方式增加了onBackPressedSupport方法,但这会引入新的基类,对业务代码的侵入性较高。

Fragivity基于androidx.activity的OnBackPressedDispatcher,以更加无侵的方式拦截back键事件。OnBackPressedDispatcher通过责任链模式保证了back事件消费的顺序,同时感知Lifecycle,在适当的时机自动注销,避免泄露。

参考:
https://developer.android.com/guide/navigation/navigation-custom-back

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    requireActivity().onBackPressedDispatcher.addCallback( this,
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // 拦截back键事件
            }
        })
}

back键返回与pop()返回


Fragivity提供pop方法,通过代码实现返回,其内部最终会调用Navigator#popBackStack。为了保证回退逻辑统一,我们希望back键回退时也由popBackStack统一处理。Navigation通过NavHostFragment进行了实现:

//NavHostFragment#onCreate
public void onCreate(@Nullable Bundle savedInstanceState) {
        //...
     mNavController = new NavHostController(context);
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        //...

}

//NavController#setOnBackPressedDispatcher
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);
}

//NavController#mOnBackPressedCallback
private final OnBackPressedCallback mOnBackPressedCallback =
        new OnBackPressedCallback(false) {
    @Override
    public void handleOnBackPressed() {
        popBackStack(); // 最终回调Navigator#popBackStack
    }
};

SwipeBack

Navigation没有提供滑动返回的能力,我们从Fragmentation中找到解决方案:onCreateView的时候,将SwipeLayout作为Container容器。使用方式非常简单:

class SwipeBackFragment : Fragment() {

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        swipeBackLayout.setEnableGesture(true//一句话开启SwipeBack
    }

}

借助ReportFragment代理,避免了额外基类的引入。swipeBackLayout是扩展属性,实际获取的是parentFragment(ReportFragment)的实例。

val Fragment.swipeBackLayout
    get() = (parentFragment as ReportFragment).swipeBackLayout

ReportFragment中的处理非常简单,将SwipeLayout作为Container即可。

internal class ReportFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    )
: View? {
        swipeBackLayout =
            SwipeBackLayout(requireContext()).apply {
                attachToFragment(
                    this@ReportFragment,
                    inflater.inflate(R.layout.report_layout, container, false)
                        .apply { appendBackground() } // add a default background color to make it opaque

                )
                setEnableGesture(false//default false
            }
        return swipeBackLayout
    }

为了避免滑动过程中的背景穿透,调用applyBackgroud()为Fragment添加与当前主题相同的默认背景色。

private fun View.appendBackground() {
    val a: TypedArray =
        requireActivity().theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
    val background = a.getResourceId(00)
    a.recycle()
    setBackgroundResource(background)
}

ShowDialog

Activity通过设置Theme可以以Dialog样式启动,使用DialogFragment同样可以实现Dialog样式的Fragment。Navigation对DialogFragment已经做了支持,Fragivity只要调用相关方法即可:

定义DialogFragment


class DialogFragment : DialogFragment() {

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

显示Dialog


navigator.showDialog(DialogFragment::class)

DialogFramgent也需要在Graph上动态添加Destination,只是与普通的Fragment有所区别,其配套的Navigator类型是DialogFragmentNavigator:

//创建Destination
val destination = DialogFragmentNavigatorDestinationBuilder(
       navigatorProvider[DialogFragmentNavigator::class],
       destIdclazz ).apply {

            label = clazz.qualifiedName
       }.build()

//添加到Graph      
graph.plusAssign(destination)

最后

Fragivity在核心逻辑上力求最大程度复用Navigation的能力,并保持与最新版本同步,这有利于保证框架的先进性和稳定性。同时Fragivity致力于打造与Activity相近的使用体验,以帮助开发者更低成本地转向单Activity架构。

工程源码中有本文介绍的各种API的demo,欢迎大家下载体验,提issue,觉得好用别忘了start~


Fragivity地址如下所示:
https://github.com/vitaviva/fragivity


·················END·················

推荐阅读

耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!

『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!

引入Jetpack架构后,你的App会发生哪些变化?(建议收藏)

重生!进阶三部曲第一部《Android进阶之光》第2版 出版!

BATcoder技术群,让一部分人先进大厂

大家,我是刘望舒,腾讯云最具价值专家TVP,著有畅销书《Android进阶之光》《Android进阶解密》《Android进阶指北》,蝉联四届电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师,百度百科收录的技术专家。

前华为面试官,现大厂技术负责人。


想要加入 BATcoder技术群,公号回复BAT 即可。

为了防止失联,欢迎关注我的小号


  微信改了推送机制,真爱请星标本公号👇
浏览 51
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报