网站首页 > 开源技术 正文
一 ViewPager2介绍
ViewPager2(以下简称VP2)是 ViewPager(以下简称VP) 库的改进版本,内部使用RecyclerView实现,可以把VP2理解为每个ItemView都充满全屏的RecyclerView,VP2可提供增强型功能并解决使用 VP 时遇到的一些常见问题。
1.1 ViewPager2特性
- 水平、垂直方向布局支持
默认是水平方向,设置VP2布局的 android:orientation="vertical" 即可轻松完成垂直方向滑动。 - RTL(right-to-left)从右到左布局支持
设置VP2布局的 android:layoutDirection="rtl" 即可。 - 一键禁止用户滑动支持
通过setUserInputEnabled()设置是否禁止用户滑动。 - 可修改的Fragment集合
VP2 支持对可修改的 Fragment 集合进行分页浏览,在底层集合发生更改时调用 notifyDatasetChanged() 来更新界面。这意味着,您的应用可以在运行时动态修改 Fragment 集合,而 VP2 会正确显示修改后的集合。 - 支持DiffUtil
VP2 在 RecyclerView 的基础上构建而成,这意味着它可以访问 DiffUtil实用程序类。所以VP2支持当数据变化时进行局部更新,而不用通过notifyDatasetChanged()全量更新。 - 支持模拟拖拽fakeDragBy
二 ViewPager2使用
2.1 基于ViewPager2实现的Banner库效果图
功能示例 | |
基本使用 | |
仿淘宝搜索栏上下轮播 |
上述示例效果源码参见:lib_viewpager2,这里只列出了实现效果图,会在下篇中进行详细介绍。
2.2 ViewPager2基本使用
- VP2不同于VP,需要单独引入:
dependencies {
implementation "androidx.viewpager2:viewpager2:1.0.0"
}
声明XML布局:
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="2:3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- 设置Adapter:
因为VP2内部是RecyclerView实现的,所以简单的界面直接继承RecyclerView.Adapter:
class VpAdapter : RecyclerView.Adapter<VpAdapter.VpViewHolder>() {
// adapter的数据源
private var data: MutableList<HouseItem> = mutableListOf()
fun setData(list: MutableList<HouseItem>) {
data.clear()
data.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VpViewHolder {
//......
}
override fun onBindViewHolder(holder: VpViewHolder, position: Int) {
}
override fun getItemCount() = data.size
class VpViewHolder(_itemView: View) : RecyclerView.ViewHolder(_itemView) {
//......
}
}
如果用到了Fragment,那么需要使用FragmentStateAdapter:
const val PAGES_NUM = 4
class ViewPager2Adapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
private val mItems: ArrayList<VP2Model> = arrayListOf()
override fun getItemCount(): Int = PAGES_NUM
override fun createFragment(position: Int): Fragment {
log("pos:$position: createFragment()")
return VP2Fragment(position)
}
override fun onBindViewHolder(
holder: FragmentViewHolder,
position: Int,
payloads: MutableList<Any>
) {
super.onBindViewHolder(holder, position, payloads)
log("pos:$position: onBindViewHolder()")
}
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun containsItem(itemId: Long): Boolean {
return super.containsItem(itemId)
}
fun setModels(newItems: List<VP2Model>) {
//不借助DiffUtil更新数据
//mItems.clear()
//mItems.addAll(newItems)
//notifyDataSetChanged()
//借助DiffUtil更新数据
val callback = PageDiffUtil(mItems, newItems)
val difResult = DiffUtil.calculateDiff(callback)
mItems.clear()
mItems.addAll(newItems)
difResult.dispatchUpdatesTo(this)
}
}
- 在Activity/Fragment中调用:
//mVP2Adapter = VpAdapter() //RecyclerView.Adapter
mVP2Adapter = ViewPager2Adapter(this) //FragmentStateAdapter
VP2.adapter = mVP2Adapter
使用起来很简单,效果图不再贴出~
2.3 进阶使用
2.3.1 Fragment懒加载
VP2使用FragmentStateAdapter加载Fragment时,是通过setOffscreenPageLimit(int limit)设置离屏缓存数量,当limit<1时,不会进行预加载,即不会回调Fragment相应的生命周期;反之会进行预加载,并回调预加载Fragment相应的生命周期,limit的默认值OFFSCREEN_PAGE_LIMIT_DEFAULT为-1,即默认就是懒加载;这一点跟VP不同,VP中默认值为1,即默认就会加载左右两侧的Fragment。
如果在VP2中既想缓存Fragment(设置setOffscreenPageLimit()的参数>=1),同时又想对数据进行懒加载(Fragment可见时才去请求数据),可以像下面这样:
/**
* 懒加载Fragment
*/
abstract class BaseLazyFragment : Fragment() {
private var mIsFirstLoad = true //是否是首次加载
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (container != null) {
val rootView = inflater.inflate(getLayoutId(), container, false)
initViews(rootView)
return rootView
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onResume() {
super.onResume()
if (mIsFirstLoad) {
initData()
mIsFirstLoad = false
}
}
@LayoutRes
protected abstract fun getLayoutId(): Int
protected fun initViews(view: View) {}
protected fun initData() {}
}
其中onResume()只会在当前Fragment可见时执行,所以用一个Boolean字段来控制只执行一次数据请求。
PS:offscreenPageLimit对mCachedViews的影响
- 当没有设置offscreenPageLimit离屏缓存时,VP2中的RecyclerView默认会在mCachedViews中缓存前面的2个Item以及后面预抓取的1个Item。
- 如果设置了offscreenPageLimit为1,则左右离屏各新增一个缓存的Item,可以认为是把画布宽度增加到3倍(左右这两个默认不可见),加上RecyclerView默认缓存的3个,除了当前显示的Item,还会缓存总共5个Item。
2.3.2 一屏多页
设置一屏多页的关键代码如下:
VP2.apply {
//下面是关键代码
val recyclerView = getChildAt(0) as RecyclerView
recyclerView.apply {
val padding = 50
// setting padding on inner RecyclerView puts overscroll effect in the right place
setPadding(padding, 0, padding, 0)
clipToPadding = false
}
adapter = Adapter()
}
在VP2源码内部第254行,RecyclerView固定索引为0:
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
所以可以通过VP2.getChildAt(0)直接获取VP2内部的RecyclerView,进而通过设置padding来实现一屏多页,运行效果如下:
一屏多页
2.3.3 ViewPager2嵌套滑动冲突
因为VP2内部是通过RecyclerView实现的,所以滑动相关处理主要在RecyclerView中进行,其内部实现:
private class RecyclerViewImpl extends RecyclerView {
RecyclerViewImpl(@NonNull Context context) {
super(context);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
}
}
onInterceptTouchEvent中会进行事件拦截,可以看到源码中的onInterceptTouchEvent只是多了isUserInputEnabled的判断,其他的都没有处理,
所以官方并没有对VP2的嵌套滑动进行处理,需要开发者进行自行处理,这里可以通过事件传递中的内部拦截法(requestDisallowInterceptTouchEvent())
进行处理,如果嵌套滑动中的内部控件需要滑动时,就控制外部父控件不拦截事件,设置为requestDisallowInterceptTouchEvent(true);反之则让外部父控件拦截事件,设置为requestDisallowInterceptTouchEvent(false)。官方Demo中也给出了对应例子:NestedScrollableHost:
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}
2.3.4 支持DiffUtil增量更新
VP2内部由RecyclerView实现,所以支持DiffUtil进行增量更新,从而提高性能;尽量避免使用notifyDatasetChanged()全量更新。DiffUtil使用方式如下:
class PageDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) :
DiffUtil.Callback() {
/**
* 旧数据
*/
override fun getOldListSize(): Int = oldModels.size
/**
* 新数据
*/
override fun getNewListSize(): Int = newModels.size
/**
* DiffUtil调用来决定两个对象是否代表相同的Item。true表示两个Item相同(表示View可以复用),false表示不相同(View不可以复用)
* 例如,如果你的项目有唯一的id,这个方法应该检查它们的id是否相等。
*/
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
}
/**
* 比较两个Item是否有相同的内容(用于判断Item的内容是否发生了改变),
* 该方法只有当areItemsTheSame (int, int)返回true时才会被调用。
*/
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldModels[oldItemPosition] == newModels[newItemPosition]
}
/**
* 该方法执行时机:areItemsTheSame(int, int)返回true 并且 areContentsTheSame(int, int)返回false
* 该方法返回Item中的变化数据,用于只更新Item中变化数据对应的UI
*/
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
return super.getChangePayload(oldItemPosition, newItemPosition)
}
}
调用方式:
//使用DiffUtil更新数据
val callback = PageDiffUtil(mItems, newItems)
val difResult = DiffUtil.calculateDiff(callback)
mItems.clear()
mItems.addAll(newItems)
difResult.dispatchUpdatesTo(adapter)
注意:如果想异步进行数据比较,可以使用AsyncListDiffer 或者RecyclerView#ListAdapter。
2.3.5 支持转场动画Transformer
调用方式:ViewPager2.setPageTransformer(transformer),如果同时想执行多个Transformer,可以像下面这样写:
val multiTransformer = CompositePageTransformer()
multiTransformer.addTransformer(ScaleInTransformer())
multiTransformer.addTransformer(MarginPageTransformer(10))
ViewPager2.setPageTransformer(multiTransformer)
三 源码浅析
3.1 RecyclerView缓存机制
因为VP2内部基于RecyclerView,所以VP2的缓存也是基于RecyclerView缓存机制实现的,直接来看RecyclerView的缓存机制:
缓存 | 涉及对象 | 作用 | 重新创建视图View(onCreateViewHolder) | 重新绑定数据(onBindViewHolder) |
一级缓存 | mAttachedScrap | 缓存屏幕中可见范围的ViewHolder | false | false |
二级缓存 | mCachedViews | 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个 | false | false |
三级缓存 | mViewCacheExtension | 开发者自行实现的缓存 | - | - |
四级缓存 | mRecyclerPool | ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder | false | true |
RecyclerView缓存机制更详细解析参见:[Android深入理解RecyclerView的缓存机制]https://blog.csdn.net/u013700502/article/details/105058771 。在VP2中主要使用的是mCachedViews、mRecyclerPool:
- mCachedViews:缓存滑动时即将与RecyclerView页面分离的ViewHolder,按子View的position或id缓存,默认存放2个,可以通过setItemViewCacheSize(int size)修改缓存个数。如果RecyclerView开启了预抓取功能(默认预抓取个数为1),则缓存池大小默认为3(mCachedViews缓存2 + 预抓取个数1 )。
- mRecyclerPool:ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder。回收到该缓存池的ViewHolder会将数据解绑,当复用该ViewHolder时,需要重新绑定数据(即重新走(onBindViewHolder)。
3.2 offscreenPageLimit离屏缓存
//ViewPager2.java
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
throw new IllegalArgumentException(
"Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
}
mOffscreenPageLimit = limit;
// Trigger layout so prefetch happens through getExtraLayoutSize()
mRecyclerView.requestLayout();
}
setOffscreenPageLimit设置的是VP2的离屏显示个数,默认是-1,因为RecyclerView中的布局是通过LayoutManager,所以真正进行离屏计算是在VP2.LinearLayoutManagerImpl#calculateExtraLayoutSpace()中,该方法计算的是LinearLayoutManager布局的额外空间,LinearLayoutManagerImpl继承自LinearLayoutManager:
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
getPageSize()表示ViewPager2的宽度,左右离屏大小都为getPageSize() * pageLimit。extraLayoutSpace[0]表示左边,extraLayoutSpace[1]表示右边。比如设置offscreenPageLimit为1,可以认为是把屏幕扩大到3倍。左右两边各有一个离屏PageSize的宽度(左右不可见),如图所示:
offscreenPageLimit
3.3 FragmentStateAdapter缓存原理
FragmentStateAdapter的使用前面已经介绍过了,因为FragmentStateAdapter继承自RecyclerView.Adapter,所以可以直接通过setAdapter设置给VP2。我们知道FragmentStateAdapter作为Adapter时,每个Item都是Fragment,那么Fragment又是怎么跟FragmentStateAdapter关联起来的呢?下面就尝试分析一下:
//FragmentStateAdapter.java
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
public abstract @NonNull Fragment createFragment(int position);
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
//FragmentViewHolder.java
public final class FragmentViewHolder extends ViewHolder {
private FragmentViewHolder(@NonNull FrameLayout container) {
super(container);
}
@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
//设置唯一ID
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
@NonNull FrameLayout getContainer() {
return (FrameLayout) itemView;
}
}
在onCreateViewHolder中设置的是名为FragmentViewHolder的ViewHolder,内部的根布局是一个FrameLayout,为该FrameLayout设置一个唯一ID,后续复用ViewHolder及Fragment的布局时会使用。FragmentStateAdapter内部两个很有用的数据结构:
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
- mFragments:是position与Fragment的映射表。随着position的增长,Fragment是会不断的新建出来的。Fragment可以被缓存起来,回收后不能重复使用,只能被重新创建。
- mItemIdToViewHolder:是position与ViewHolder#Id的映射表。由于ViewHolder是RecyclerView缓存机制的载体,所以随着position的增长,ViewHolder会被重新利用。
当VP2滑动时,当前屏幕正在显示的前面最近的2个Item会被缓存到mCachedViews中,超过2个时会从mCachedViews删除,并将其转移到RecyclerPool中,此时会调用onViewRecycled()如下:
@Override
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
}
当ViewHolder回收到RecyclerPool中时,将ViewHolder相关的信息删除。在前面的介绍中我们知道从mCachedViews中取ViewHolder时并不会执行onBindViewHolder,只有从RecyclerPool取ViewHolder时才会执行到onBindViewHolder,接着看一下onBindViewHolder:
@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
//如果mItemIdToViewHolder中跟当前ViewHolder的ID一样,那么需要将mItemIdToViewHolder中的ID进行删除,并在后面重新对该ViewHolder的ID进行赋值
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
//在这里将viewHolerId重新添加到mItemIdToViewHolder中
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
//创建Fragment并添加到mFragments中
ensureFragment(position);
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
//...其他...
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
//将新来的Fragment的布局依附到ViewHolder中
placeFragmentInViewHolder(holder);
}
}
});
}
gcFragments();
}
//onBindViewHolder()中调用该方法创建Fragment
private void ensureFragment(int position) {
long itemId = getItemId(position);
if (!mFragments.containsKey(itemId)) {
//在这里创建Fragment
Fragment newFragment = createFragment(position);
newFragment.setInitialSavedState(mSavedStates.get(itemId));
mFragments.put(itemId, newFragment);
}
}
可以看到在onBindViewHolder()中创建了Fragment并将其添加到了mFragments中,从而Fragment跟FragmentStateAdapter关联起来了。
默认当前Item的前面2个及后面的1个(RecyclerView默认会开启预抓取能力:isItemPrefetchEnabled默认为true)总共3个Fragment会缓存在mCachedViews中;超过2个的位置时创建的Fragment就会被销毁,有一种特殊情况需要注意:当VP2滑动到最后时,当前Item前面的3个(这里不是默认的2个了)Fragment都会被缓存,因为滑动到最后了,后面预抓取的1个给到了前面。当第一次加载时,由于还没有触发VP2的onTouch操作,所以此时还不会进行后面的预抓取。
四 ViewPager、ViewPager2差异对比
功能 | ViewPager | ViewPager2 |
Listener | addPageChangeListener | registerOnPageChangeCallback(OnPageChangeCallback callback),其中OnPageChangeCallback是一个抽象类,不同于接口方式,抽象类里用到哪个覆写哪个即可 |
Fragment | FragmentPagerAdapter、FragmentStatePagerAdapter | FragmentStateAdapter |
setOffscreenPageLimit(int num) | 离屏缓存,当设置小于1时,会强制设为1,即强制左右各缓存1个 | OFFSCREEN_PAGE_LIMIT_DEFAULT默认为-1,及默认不会离屏缓存 |
Adapter | PagerAdapter | RecyclerView.Adapter |
其他操作 | / | 支持RTL从右到左排序、垂直滑动、停止用户操作 |
五 参考
【1】[官方:使用 ViewPager2 在 Fragment 之间滑动]:https://developer.android.com/training/animation/screen-slide-2?hl=zh-cn
【2】[官方:从 ViewPager 迁移到 ViewPager2]https://developer.android.com/training/animation/vp2-migration?hl=zh-cn
【3】[ViewPager2中的Fragment懒加载实现方式]https://blog.csdn.net/qq_36486247/article/details/103959356
【4】[聊聊ViewPager2中的缓存和复用机制]https://mp.weixin.qq.com/s/9vNIBA7s647-7C4YTZ8QGw
猜你喜欢
- 2024-12-12 SpringBoot如何快速集成RocketMQ
- 2024-12-12 译|Python幕后(3):漫步CPython源码
- 2024-12-12 心房颤动伴长RR间期及散点图
- 2024-12-12 心房颤动及长间歇
你 发表评论:
欢迎- 05-16东契奇:DFS训练时喷了我很多垃圾话 我不懂他为什么比赛不这么干
- 05-16这两球很伤!詹姆斯空篮拉杆不中 DFS接里夫斯传球空接也没放进
- 05-16湖人自媒体调查:89%球迷希望DFS回归79%希望詹姆斯回归
- 05-16Shams:湖人得到全能球员DFS 节省了1500万奢侈税&薪金空间更灵活
- 05-16G5湖人胜率更高!詹姆斯不满判罚,DFS谈5人打满下半场:这很艰难
- 05-16DFS:当东契奇进入状态 所有防守者在他面前都像个圆锥桶
- 05-16上一场9中6!DFS:不能让纳兹-里德这样的球员那么轻松地投三分
- 05-16WIDER FACE评测结果出炉:滴滴人脸检测DFS算法获世界第一
- 最近发表
-
- 东契奇:DFS训练时喷了我很多垃圾话 我不懂他为什么比赛不这么干
- 这两球很伤!詹姆斯空篮拉杆不中 DFS接里夫斯传球空接也没放进
- 湖人自媒体调查:89%球迷希望DFS回归79%希望詹姆斯回归
- Shams:湖人得到全能球员DFS 节省了1500万奢侈税&薪金空间更灵活
- G5湖人胜率更高!詹姆斯不满判罚,DFS谈5人打满下半场:这很艰难
- DFS:当东契奇进入状态 所有防守者在他面前都像个圆锥桶
- 上一场9中6!DFS:不能让纳兹-里德这样的球员那么轻松地投三分
- WIDER FACE评测结果出炉:滴滴人脸检测DFS算法获世界第一
- 湖人自媒体调查:89%球迷希望DFS回归 79%希望詹姆斯回归
- 一觉醒来湖人苦盼的纯3D终于到位 DFS能带给紫金军多少帮助
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)