摘要:预热是异步进行的,返回值表示请求是否被接收。多个成功的请求都会返回。连接回调在中,导航事件发生时被调用。其他的浏览器使用提供相同的定制也是有可能的。怎样检测是否支持所有支持的浏览器都暴露了一个。确保提供了备选方案以提供好的应用体验。
前言
文章比较长,先放项目地址:PaperPlane
俗话说,没图说个那啥,先看实际效果。
什么是Custom Tabs?所有的APP开发者都面临这样一个选择,当用户点击一个URL时,是应该用浏览器打开还是应该用应用内置的WebView打开呢?
两个选项都面临着一些问题。通过浏览器打开是一个非常重的上下文切换,并且是无法定制的。而WebView不能和浏览器共享数据并且需要需要手动去处理更多的场景。
Chrome Custom Tabs让APP在进行网页浏览时更多的控制权限,在不采用WebView的情况下,这既能保证Native APP和网页之间流畅的切换,又允许APP定制Chrome的外观和操作。可定义的内容如下:
toolbar的颜色
进场和退场动画
给Chrome的toolbar、overflow menu和bottom toolbar添加自定义操作
并且,Chrome Custom Tabs允许开发者对Chrome进行预启动和网页内容的预加载,以此提升加载的速度。
Chrome Custom Tabs VS WebView, 我应该什么时候用?如果页面的内容是由我们自己控制的,可以和Android组件进行交互,那么,WebView是一个好的选择,如果我们的应用需要打开外部的网站,那么推荐使用Chrome Custom Tabs,原因如下:
导入非常简单。不需要编写额外的代码来管理请求,授予权限或者存储cookie
定制UI:
Toolbar 颜色
动作按钮 (Action Button)
定制菜单项
定制进场退场动画
Bottom Toolbar
导航感知:浏览器通知回调接口通知应用网页的导航情况
安全性:浏览器使用了Google"s Safe Browsing,用于保护用户和设备免受危险网站的侵害
性能优化:
浏览器会在后台进行预热,避免了应用占用大量资源的情况
提前向浏览器发送可能的URL,提高了页面加载速度
生命周期管理:在用户与Custom Tabs进行交互时,浏览器会将应用标示为前台应用,避免了应用被系统所回收
共享cookie数据和权限,这样,用户在已经授权过的网站,就不需要重新登录或者授权权限了
如果用户开启了数据节省功能,在这里仍然可以从中受益
同步的自动补全功能
仅仅需要点击一下左上角按钮就可以直接返回原应用
想要在Lollipop之前的设备上最新引入的浏览器(Auto updating WebView),而不是旧版本的WebView
什么时候可用?从Chrome 45版本开始,所有的Chrome用户都可以使用这项功能,目前仅支持Android系统。
开发指南完整的示例可以查看https://github.com/GoogleChrome/custom-tabs-client。包含了定制UI、连接后台服务、处理应用和Custom Tab Activity生命周期的可复用的类。
第一步当然是将 Custom Tabs Support Library 添加到工程中来。打开build.gradle文件,添加support library的依赖。
dependencies { ... compile "com.android.support:customtabs:23.3.0" }
一旦Support Library添加项目成功了,我们就有两种可能的定制操作了:
定制UI和与Chrome Custom Tabs的交互
使页面加载更快速,保持应用激活
UI的定制是通过使用 CustomTabsIntent 和 CustomTabsIntent.Builder类完成的;而速度的提升则是通过使用 CustomTabsClient 链接Custom Tabs服务,预热Chrome和让Chrome知晓将要打开的URL实现的。
打开一个Chrome Custom Tab// 使用CustomTabsIntent.Builder配置CustomTabsIntent // 准备完成后,调用CustomTabsIntent.Builder.build()方法创建一个CustomTabsIntent // 并通过CustomTabsIntent.launchUrl()方法加载希望加载的url String url = ¨https://github.com/marktony¨; CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); CustomTabsIntent customTabsIntent = builder.build(); customTabsIntent.launchUrl(this, Uri.parse(url));配置地址栏的颜色
Chrome Custom Tabs一个很重要的功能就是我们能够改变地址栏的颜色,使之和我们应用的颜色协调。
// 改变toolbar的背景色。colorInt就是想要指定的int值 builder.setToolbarColor(colorInt);配置定制化的action button
作为应用的开发者,我们对呈现在用户眼前的Chrome Custom Tab内的Action Button拥有完全的控制权。
在大部分的情况下,用户会执行最基础的操作像分享,或者是其他公共的Activity。
Action Button被表示为一个action button的图标和用户点击action button之后Chrome将要执行的pendingIntent。图标的高度一般为24dp,宽度一般为24-48dp。
// 向toolbar添加一个Action Button // ‘icon’是一张位图(Bitmap),作为action button的图片资源使用 // "description"是一个字符串,作为按钮的无障碍描述所使用 // "pendingIntent" 是一个PendingIntent,当action button或者菜单项被点击时调用。 // 在url作为data被添加之后,Chrome 会调用PendingIntent#send()方法。 // 客户端应用会通过调用Intent#getDataString()获取到URL // "tint"是一个布尔值,定义了Action Button是否应该被着色 builder.setActionButton(icon, description, pendingIntent, tint);配置定制化菜单
Chrome浏览器拥有非常全面的action菜单,用户在浏览器内操作非常顺畅。然而,对于我们自己的应用,可能就不适合了。
Chrome Custom Tabs顶部有三个横向排列的图标,分别是“前进”、"页面信息"和”刷新“。在菜单的底部分别是"查找页面"和“在浏览器中打开”。
作为开发者,我们最多可以在顶部横向图标和底部菜单之间添加5个自定义菜单选项。
菜单项通过调用CustomTabsIntent.Builder#addMenuItem)添加,title和用户点击菜单选项后Chrome调用的pendingIntent需要作为参数被传入。
builder.addMenuItem(menuItemTitle, menuItemPendingIntent);配置进场和退场动画
许多的Android都会在Activity之间切换时使用自定义的视图进入和退出动画。Chrome Custom Tabs也一样,我们可以改变进入和退出动画,以此保持Chrome Custom Tabs和应用其他内容的协调性和一致性。
builder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left); builder.setExitAnimations(this, R.anim.slide_in_left, R.anim.slide_out_right);预热Chrome,提高页面加载速度
默认情况下,当 CustomTabsIntent#launchUrl )被调用时会激活Chrome,加载URL。这会花费我们宝贵的时间并且影响流畅度。
Chrome团队了解用户对于流畅体验的渴望,所以他们在Chrome中提供了一个Service使我们的APP能够连接并且预热浏览器和原生组件。他们也把这种能力分享给了我们普通开发者,开发者能够告知Chrome用户访问页面的可能性。然后,Chrome就能完成如下的操作:
主域名的DNS预解析
最有可能加载的资源的DNS预解析
包括HTTPS/TLS验证在内的预连接
预热Chrome的步骤如下:
使用CustomTabsClient#bindCustomTabsService)连接service
一旦service连接成功,后台调用 CustomTabsClient#warmup)启动Chrome
调用 CustomTabsClient#newSession )创建一个新的session.这个session被用作所有的API请求
我们可以在创建session时选择性的添加一个 CustomTabsCallback作为参数,这样我们就能知道页面是否被加载完成
通过 CustomTabsSession#mayLaunchUrl)告知Chrome用户最有可能加载的页面
调用 CustomTabsIntent.Builder 构造方法,并传入已经创建好的CustomTabsSession作为参数传入
连接Chrome服务CustomTabsClient#bindCustomTabsService http://developer.android.com/... java.lang.String, android.support.customtabs.CustomTabsServiceConnectio)) 方法简化了连接Custom Tabs服务的过程。
创建一个继承自CustomTabsServiceConnection的类并使用onCustomTabsServiceConnected )方法获取 CustomTabsClient的实例。在下一步中会用到此实例:
// 官方示例 // 客户端需要连接的Chrome的包名,取决于channel的名称 // Stable(发行版) = com.android.chrome // Beta(测试版) = com.chrome.beta // Dev(开发版) = com.chrome.dev public static final String CUSTOM_TAB_PACKAGE_NAME = "com.android.chrome"; // Change when in stable CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { @Override public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { mCustomTabsClient = client; } @Override public void onServiceDisconnected(ComponentName name) { } }; boolean ok = CustomTabsClient.bindCustomTabsService(this, mPackageNameToBind, connection);
// 我的示例 package com.marktony.zhihudaily.customtabs; import android.support.customtabs.CustomTabsServiceConnection; import android.content.ComponentName; import android.support.customtabs.CustomTabsClient; import java.lang.ref.WeakReference; /** * Created by Lizhaotailang on 2016/9/4. * Implementation for the CustomTabsServiceConnection that avoids leaking the * ServiceConnectionCallback */ public class ServiceConnection extends CustomTabsServiceConnection { // A weak reference to the ServiceConnectionCallback to avoid leaking it. private WeakReference预热浏览器mConnectionCallback; public ServiceConnection(ServiceConnectionCallback connectionCallback) { mConnectionCallback = new WeakReference<>(connectionCallback); } @Override public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); if (connectionCallback != null) connectionCallback.onServiceConnected(client); } @Override public void onServiceDisconnected(ComponentName name) { ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); if (connectionCallback != null) connectionCallback.onServiceDisconnected(); } }
boolean warmup(long flags))
预热浏览器进程并加重原生库文件。预热是异步进行的,返回值表示请求是否被接收。多个成功的请求都会返回true。
true代表着成功。
创建新的tab sessioinboolean newSession(CustomTabsCallback callback))
session用于在连续请求中链接mayLaunchUrl方法。CustomTabsIntent和tab互相联系。这里所提供的回调和已经创建成功的session相关。通过这个回调,任何关于已经成功创建的session的更新都会被接收到。返回session是否被成功创建。多个具有相同CustomTabsCallback或者null值的请求都会返回false。
告知Chrome用户可能打开的链接boolean mayLaunchUrl(Uri url, Bundle extras, List otherLikelyBundles))
CustomTabsSession方法告知浏览器未来可能导航到的url。warmup())方法应该先被调用。最有可能的url应该最先被指出。也可以选择性的提供可能加载的url的列表。列表中的数据被认为被加载的可能性小于最初的那一个,而且必须按照优先级降序排列。这些额外的url可能被忽略掉。所有之前对于这个方法的调用都会被去优先化。返回操作是否成功完成。
Custom Tabs连接回调void onNavigationEvent(int navigationEvent, Bundle extras))
在custom tab中,导航事件发生时被调用。‘navigationEvent int’是关于页面内的6个状态值之一。6个状态值定义如下:
/** * 页面开始加载时被发送 */ public static final int NAVIGATION_STARTED = 1; /** * 页面完成加载时被发送 */ public static final int NAVIGATION_FINISHED = 2; /** * 由于错误tab不能完成加载时被发送 */ public static final int NAVIGATION_FAILED = 3; /** * 在加载完成之前,加载因为用户动作例如点击了另外一个链接或者刷新页面 * 加载被中止时被发送 */ public static final int NAVIGATION_ABORTED = 4; /** * tab状态变为可见时发送 */ public static final int TAB_SHOWN = 5; /** * tab状态变为不可见时发送 */ public static final int TAB_HIDDEN = 6;如果用户没有安装最新版本的Chrome,会发生什么呢?
Custom Tabs通过带有key Extras的 ACTION_VIEW Intent来定制UI。这就意味着将要打开的页面会通过系统浏览器或者用户默认浏览器打开。
如果用户已经安装了Chrome并且是默认浏览器,它会自动的获取EXTRAS的值并提供一个定制化的UI。其他的浏览器使用Intent extras提供相同的定制UI也是有可能的。
怎样检测Chrome是否支持Chrome Custom Tabs?所有支持Chrome Custom Tabs的Chrome浏览器都暴露了一个service。为了检测是否支持Chrome Custom Tabs,可以尝试着绑定service,如果成功的话,那么Customs Tabs可以成功的使用。
最佳实践启用Chrome Custom Tabs后,我们看到了各种不同质量界别的实现效果。这里介绍一组实现优秀集成的最佳实践。
连接Custome Tabs Service并发起预热连接到Custom Tabs Service并预加载Chrome之后,通过Custom Tabs打开链接 最多可以节省700ms 。
在我们打算启用Custom Tabs的Activity的 onStart()) 方法中连接 Custom Tabs service。连接成功后,调用warmup()方法。
Custom Tabs作为一个非常低优先级的进程,这也就意味着 它不会对我们的应用不会有任何的负面的影响,但是当加载链接时,会获得非常好的启动性能。
内容预渲染预渲染让内容打开非常迅速。所以,如果用户 至少有50%的可能性 打开某个链接,调用mayLaunchUrl()
方法。
调用mayLaunchUrl()
方法方法能使Custom Tabs预获取主页面所支持的内容并预渲染。这会最大程度的加快页面的加载速度。但是会不可避免的有 一点流量和电量的消耗。
Custom Tabs非常的智能,能够感知用户是否在使用收费的网络或者设备电量不足,预渲染对设备的整体性能有负面的影响,在这样的场景下,Custom Tabs就不会进行预获取或者预渲染。所以,不用担心应用的性能问题。
当Custom Tabs没有安装时,提供备选方案尽管Custom Tabs对于大多数用户都是适用的,仍然有一些场景不适用,例如设备上没有安装支持Custom Tabs的浏览器或者是设备上的浏览器版本不支持Custom Tabs。
确保提供了备选方案以提供好的应用体验。打开默认浏览器或者引入WebView都是不错的选择。
将我们的应用作为referrer(引荐来源)通常,对于网站而言,追用访问的来源非常地重要。当加载了Custom Tabs时,通过设置referrer,让他们知晓我们正在给他们提高访问量。
intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(Intent.URI_ANDROID_APP_SCHEME + "//" + context.getPackageName()));添加定制动画
定制的动画能够使我们的应用切换到网页内容时更加地顺畅。 确保进场动画和出厂动画是反向的,这样能够帮助用户理解跳转的关系。
//设置定制的进入/退出动画 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left); intentBuilder.setExitAnimations(this, android.R.anim.slide_in_left, android.R.anim.slide_out_right); //打开Custom Tab intentBuilder.build().launchUrl(context, Uri.parse("https://github.com/marktony"));为Action Button选择一个icon
添加一个Action Button能够使用户更加理解APP的功能。但是,如果没有好的icon代表Action Button将要执行的操作,有必要创建一个带操作文字描述的位图。
牢记位图的最大尺寸为高度24dp,宽度48dp。
String shareLabel = getString(R.string.label_action_share); Bitmap icon = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_share); // 为我们的BroadCastReceiver创建一个PendingIntent Intent actionIntent = new Intent( this.getApplicationContext(), ShareBroadcastReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, actionIntent, 0); // 设置pendingIntent作为按钮被点击后将要执行的操作 intentBuilder.setActionButton(icon, shareLabel, pendingIntent);为其他浏览器做准备
牢记用户安装的浏览器中,支持Custom Tabs的数量可能不止一个。如果有不止一个浏览器支持Custom Tabs,并且没有任何一个浏览器被设置为偏好浏览器,需要询问用户如何打开链接
/** * 返回支持Custom Tabs的应用的包名 */ public static ArrayList getCustomTabsPackages(Context context) { PackageManager pm = context.getPackageManager(); // Get default VIEW intent handler. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); // 获取所有能够处理VIEW intents的应用 List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); ArrayList packagesSupportingCustomTabs = new ArrayList<>(); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); // Check if this package also resolves the Custom Tabs service. if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs.add(info); } } return packagesSupportingCustomTabs; }允许用户不使用Custom Tabs
为应用添加一个设置选项,允许用户通过默认浏览器而不是Custom Tab打开链接。如果我们的应用在添加Custom Tabs之前,都是通过默认浏览器打开链接显得尤为重要。
尽量让Native应用处理URLNative应用可以处理一些url。如果用户安装了Twitter APP,在点击tweet内的链接时,她更加希望Twitter应用能够处理这些链接。
在应用内打开链接之前,检查手机里有没有其他APP能够处理这些url。
定制Toolbar的颜色如果想要让用户感觉网页内容是我们应用的一部分,将toolbar的颜色设置为primaryColor。
如果想要让用户清楚的了解到已经离开了我们的应用,那就完全不要定义toolbar的颜色。
// 设置自定义的toolbar的颜色 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setToolbarColor(Color.BLUE);增加分享按钮
确保在overflow菜单中添加了一个分享的操作,在大多数的情况下,用户希望能够分享当前所见网页内容的链接,Custom Tabs默认没有添加分享的按钮。
// 在BroadCastReceiver中分享来自CustomTabs的内容 public void onReceive(Context context, Intent intent) { String url = intent.getDataString(); if (url != null) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, url); Intent chooserIntent = Intent.createChooser(shareIntent, "Share url"); chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(chooserIntent); } }定制关闭按钮
自定义关闭按钮使CustomTabs看起来像应用的一部分。
如果希望CustomTabs在用户看来像一个Dialog, 使用"x"(叉叉)按钮。如果希望Custom Tab是用户的一部分,使用返回箭头。
//设置自定义的关闭按钮 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setCloseButtonIcon(BitmapFactory.decodeResource( getResources(), R.drawable.ic_arrow_back));处理内部链接
当监听到链接是由android:autoLink生成的或者在WebView中复写了click方法,确保我们的应用处理了这些内容的链接,让CustomTabs处理外部链接。
WebView webView = (WebView)findViewById(R.id.webview); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } @Override public void onLoadResource(WebView view, String url) { if (url.startsWith("http://www.example.com")) { //Handle Internal Link... } else { //Open Link in a Custom Tab Uri uri = Uri.parse(url); CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(mCustomTabActivityHelper.getSession()); //Open the Custom Tab intentBuilder.build().launchUrl(context, url)); } } });处理连击
如果希望在用户点击链接和打开CustomTabs之间做一些准备工作,确保所花费的时间不超过100ms。否则用户会认为APP没有响应,可能是试着点击链接多次。
如果不能避免延迟,确保我们的应用对可能的情况做好准备,当用户点击相同的链接多次时,不要多次打开CustomTab。
低版本API尽管整合Custom Tabs的推荐方式是使用Custom Tabs Support Library,低API版本的系统也是可以使用的。
完整的Support Library的导入方法可以参见GitHub,并可以做为一个起点。连接service的AIDL文件也被包含在其中,Chromium仓库中也包含了这些文件,而这些文件在Android Studio中是不能直接被使用的。
在低版本API中使用Custom Tabs的基本方法String url = ¨https://github.com/marktony¨; Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); private static final String EXTRA_CUSTOM_TABS_SESSION = "android.support.customtabs.extra.SESSION"; Bundle extras = new Bundle; extras.putBinder(EXTRA_CUSTOM_TABS_SESSION, sessionICustomTabsCallback.asBinder() /* 不需要session时设置为null */); intent.putExtras(extras);定制UI
UI定制是通过向ACTION_VIEW Intent添加Extras实现的。用于定制UI的完整的extras keys的列表可以在 CustomTabsIntent docs 找到。下面是添加自定义的toolbar的颜色的示例:
private static final String EXTRA_CUSTOM_TABS_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR"; intent.putExtra(EXTRA_CUSTOM_TABS_TOOLBAR_COLOR, colorInt);连接Custom Tabs Service
Custom Tabs service和其他Android Service的使用方法相同。接口通过AIDL创建并且代理service类也会自动创建。
// 客户端需要连接的Chrome的包名,取决于channel的名称 // Stable(发行版) = com.android.chrome // Beta(测试版) = com.chrome.beta // Dev(开发版) = com.chrome.dev public static final String CUSTOM_TAB_PACKAGE_NAME = "com.chrome.dev"; // Change when in stable public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; Intent serviceIntent = new Intent(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(CUSTOM_TAB_PACKAGE_NAME); context.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);一些有用的网址
Github Demo
Chrome Custom Tabs on StackOverflow
我的项目地址:PaperPlane
总结如果我们的应用是面向国外用户的,那理所当然的,应该加入Chrome Custom Tabs的支持,这在很大程度上能够提升用户的体验。如果我们的应用只是面向国内用户,我的建议还是应该加上这项功能,毕竟,还是有部分用户安装了Chrome浏览器,当用户浏览到Custom Tab页面,应该也会像我一样,感觉到眼前一亮吧。
文章比较长,感谢阅读。
本文章由简书用户TonnyL原创,转载请注明作者、出处以及链接。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/66453.html
摘要:咦,为什么不做一个插件用来管理呢每次同时打开过多的选项卡时,被挤压的标题总是让我分不清哪个是哪个,查看起来十分不便。即数据中的属性,因此关闭选项卡的功能实现起来也没有问题。 1. 前言 继上周第一次开发Chrome插件github-star-trend之后,我就一直寻思有什么现实问题可以用插件来解决呢?正当我在浏览器中搜索寻找灵感时,打开的众多tab选项卡令我灵光一闪。 咦,为什么不做...
摘要:在上看到发的视频被狂转开始注意之前几乎对这个词语没有印象看到是在的演讲还以为是新技术在上找一下这次好多个视频是关于的视频的内容主要是讲网站优化分别用做例子可惜没有大概要等小右补方案应该没有问题从视频看优化的效果非常显著本来好几秒的 在 Twitter 上看到 Addy Osmani 发的视频被狂转, 开始注意https://twitter.com/addyosmani/status/7...
摘要:关于我的博客掘金专栏路易斯专栏原文链接扩展开发定制请求响应头域本文共字,阅读需分钟。那么,我会放弃吗反向代理显然不会,既然问题出在上,我去掉就行了。然而无论多少次的学习和模仿,最终的目的还是为了使用,故开发一款定制请求的势在必行。 本文首发于《程序员》杂志2017年第9、10、11期,下面的版本又经过进一步的修订。 关于 Github:IHeader 我的博客:louis blog ...
摘要:实现方案对页面中涉及文案进行修改,绑定多语言值。利用插件支持跨站请求的特性,向多语言平台直接发送修改请求。异常处理利用插件可以获取浏览器中特性,新开一个标签页打开多语言后台,进行登录,登录成功后就可以实现请求的授权修改了。 一、前言提起Chrome扩展插件(Chrome Extension),每个人的浏览器中或...
阅读 841·2021-11-16 11:56
阅读 1653·2021-11-16 11:45
阅读 3108·2021-10-08 10:13
阅读 4094·2021-09-22 15:27
阅读 726·2019-08-30 11:03
阅读 642·2019-08-30 10:56
阅读 945·2019-08-29 15:18
阅读 1737·2019-08-29 14:05