原文参考:自定义控件的惯性滑动

体验RecyclerView的滑动以及滚动的实现源码

一、应用场景

在自定义View中,常常会用到滚动,但是出于某些原因不能直接继承ScrollView,这时候就很有必要来看看他们滚动都是怎么实现的了。
本文只关注拖动和惯性滑动的效果实现。以RecyclerView的代码为示例(和ScrollView相比,在滚动上的实现方式一样,在惯性滑动的实现上,用的插值器(Interpolator)不同,下文会讲到),抽出RecyclerView中的手指拖动和手指离开后的惯性滑动的代码。

二、效果展示

继承ViewGroup,实现RecyclerView的滑动效果,如图:

三、核心效果概述

  • 单指拖动
  • 多指操作时,以新加入的手指为准进行拖动
  • 手指松开时的惯性滑动
  • 滑动到边缘时的处理

四、效果实现

首先,先在onLayout里面加上20个View用来展示拖动的效果(这一部分和滑动无关,只为效果展示,可跳过),这里给出效果图:

共有20个Item,由于还没加滑动,暂时只能显示前两个Item。

4.1 单指拖动

针对用户操作,这时候自然就要用到onTouchEvent()了,区分开用户的按下,移动,抬起操作。
在这之前需要先定义一个常亮mTouchSlop,当手指移动大于这个常量,便表示手指开始拖动,否则,表示手指仅仅只是按下,这样可以更好的区别出用户的意图。

final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();

下面放出单指拖动的onTouchEnevt()的代码:

public static final int SCROLL_STATE_IDLE = 0;
public static final int SCROLL_STATE_DRAGGING = 1;
private int mLastTouchY;@Override
public boolean onTouchEvent(MotionEvent event) {final int action = MotionEventCompat.getActionMasked(event);switch (action) {case MotionEvent.ACTION_DOWN: {mScrollState = SCROLL_STATE_IDLE;mLastTouchY = (int) (event.getY() + 0.5f);break;}case MotionEvent.ACTION_MOVE: {int y = (int) (event.getY() + 0.5f);int dy = mLastTouchY - y;if (mScrollState != SCROLL_STATE_DRAGGING) {boolean startScroll = false;if (Math.abs(dy) > mTouchSlop) {if (dy > 0) {dy -= mTouchSlop;} else {dy += mTouchSlop;}startScroll = true;}if (startScroll) {mScrollState = SCROLL_STATE_DRAGGING;}}if (mScrollState == SCROLL_STATE_DRAGGING) {mLastTouchY = y;scrollBy(0, dy);}break;}case MotionEvent.ACTION_UP: {break;}}return true;
}

上面的代码和变量都是来自RecyclerView的onTouchEvent方法,当然我剔除了单指拖动以外的部分。稍微解释一下主要的思路:
在用户事件:DOWN -> MOVE -> MOVE -> ... -> MOVE -> UP中,首先在DOWN中记录下按下的位置,在每一个MOVE事件中计算和DOWN之间的位置差,当有一个MOVE的位置差大于最小移动距离(mTouchSlop)时,表示拖动开始,开始位移。之后的MOVE事件也无需再次和mTouchSlop比较,直接进行拖动位移,直到UP事件触发。
这时候就存在一个问题,如图:

存在两个手指时,以第一个手指操作为准,当第一个手指松开时,会跳到第二个手指按下时的位置。

4.2 多指操作

多指滑动时就需要指明,控件到底该听谁的。这里就需要有个约束:

  • 以新加入的手指的滑动为准
  • 当有一个手指抬起时,以剩下的手指的滑动为准

要做到上面的约束,就不可避免的需要区分出屏幕上的手指。MotionEvent提供getPointerId()方法,用于返回每一个手指的ID。

先奉上添加了多指操作后的onTouchEvent方法的代码:

private static final int INVALID_POINTER = -1;
private int mScrollPointerId = INVALID_POINTER;
@Override
public boolean onTouchEvent(MotionEvent event) {final int action = MotionEventCompat.getActionMasked(event);final int actionIndex = MotionEventCompat.getActionIndex(event);switch (action) {case MotionEvent.ACTION_DOWN: {setScrollState(SCROLL_STATE_IDLE);mScrollPointerId = event.getPointerId(0);mLastTouchY = (int) (event.getY() + 0.5f);break;}case MotionEventCompat.ACTION_POINTER_DOWN: {mScrollPointerId = event.getPointerId(actionIndex);mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);break;}case MotionEvent.ACTION_MOVE: {final int index = event.findPointerIndex(mScrollPointerId);if (index < 0) {Log.e("zhufeng", "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?");return false;}final int y = (int) (event.getY(index) + 0.5f);int dy = mLastTouchY - y;if (mScrollState != SCROLL_STATE_DRAGGING) {boolean startScroll = false;if (Math.abs(dy) > mTouchSlop) {if (dy > 0) {dy -= mTouchSlop;} else {dy += mTouchSlop;}startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}if (mScrollState == SCROLL_STATE_DRAGGING) {mLastTouchY = y;scrollBy(0, dy);}break;}case MotionEventCompat.ACTION_POINTER_UP: {if (event.getPointerId(actionIndex) == mScrollPointerId) {// Pick a new pointer to pick up the slack.final int newIndex = actionIndex == 0 ? 1 : 0;mScrollPointerId = event.getPointerId(newIndex);mLastTouchY = (int) (event.getY(newIndex) + 0.5f);}break;}case MotionEvent.ACTION_UP: {break;}}return true;
}

添加了一个新的变量mScrollPointerId,用于指定当前移动遵循的是哪一个手指的操作,在有新的手指加入时,设置mScrollPointerId为新的手指。在有手指离开的时候,设置mScrollPointerId为剩下的那个手指。
添加了ACTION_POINTER_DOWN和ACTION_POINTER_UP两个事件,在已有DOWN事件后,新增手指点击便会出发ACTION_POINTER_DOWN事件,ACTION_POINTER_DOWN和ACTION_POINTER_UP类似于DOWN和UP事件,都是成对出现。区别在于,DOWN和UP是第一个手指,ACTION_POINTER_DOWN和ACTION_POINTER_UP,只要有一个新的手指加入,就会触发一次。
核心的就是明确当前实际操作的手指(mScrollPointerId),计算位置信息都使用mScrollPointerId的手指即可保证位移信息的正确性。
也来给出个应有的效果:

4.3 惯性滑动

要做到惯性滑动,我们需要做到:

  • 得到手指抬起时的速度
  • 将速度转换成具体的位移

4.3.1 获取速度

首先,关于如何在ACTION_UP中得到速度。VelocityTrackerCompatgetYVelocity可以获得指定ID的手指当前Y轴上的速度。向上为负,向下为正。关于VelocityTrackerCompat和VelocityTracker的使用,这里直接贴出:

private VelocityTracker mVelocityTracker;@Override
public boolean onTouchEvent(MotionEvent event) {if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}boolean eventAddedToVelocityTracker = false;final MotionEvent vtev = MotionEvent.obtain(event);...case MotionEvent.ACTION_UP: {mVelocityTracker.addMovement(vtev);eventAddedToVelocityTracker = true;mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);float yVelocity = -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId);...}if (!eventAddedToVelocityTracker) {mVelocityTracker.addMovement(vtev);}vtev.recycle();...
}

4.3.2 将速度反应到滑动上

重点说一下如何将UP时的速度,反应到控件的滚动上。根据OverScroller的fling方法,我们不去探寻这个方法具体是如何实现的,只需要知道,调用了这个方法之后,便可以不停的去调用getCurrY()方法,询问当前移动到哪儿了,知道这次的滑动停止(computeScrollOffset方法返回false)。
这样我们要做的就是:

  1. 在UP的时候获取Y轴上的移动速度
  2. 判断时候需要惯性滑动
  3. 需要惯性滑动的时候调用OverScrollerfling方法,进行模拟滑动计算
  4. 在滑动停止之前不停的询问当前按照计算来说应该滑动到哪儿了去设置控件的位置

需要明确的一个概念,OverScroller方法,只涉及到滑动位置的计算,根据输入的值,计算在什么时间应该滑动到什么位置,具体的控件的移动还是需要调用View的ScrollTo或者ScrollBy方法。

落实到代码中就是:

private class ViewFlinger implements Runnable {private int mLastFlingY = 0;private OverScroller mScroller;private boolean mEatRunOnAnimationRequest = false;private boolean mReSchedulePostAnimationCallback = false;public ViewFlinger() {mScroller = new OverScroller(getContext(), sQuinticInterpolator);}@Overridepublic void run() {disableRunOnAnimationRequests();final OverScroller scroller = mScroller;if (scroller.computeScrollOffset()) {final int y = scroller.getCurrY();int dy = y - mLastFlingY;mLastFlingY = y;scrollBy(0, dy);postOnAnimation();}enableRunOnAnimationRequests();}public void fling(int velocityY) {mLastFlingY = 0;setScrollState(SCROLL_STATE_SETTLING);mScroller.fling(0, 0, 0, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);postOnAnimation();}public void stop() {removeCallbacks(this);mScroller.abortAnimation();}private void disableRunOnAnimationRequests() {mReSchedulePostAnimationCallback = false;mEatRunOnAnimationRequest = true;}private void enableRunOnAnimationRequests() {mEatRunOnAnimationRequest = false;if (mReSchedulePostAnimationCallback) {postOnAnimation();}}void postOnAnimation() {if (mEatRunOnAnimationRequest) {mReSchedulePostAnimationCallback = true;} else {removeCallbacks(this);ViewCompat.postOnAnimation(CustomScrollView.this, this);}}
}

这里用的是RecyclerView中的惯性滑动的代码,剔除了一些不需要的部分。

public void fling(int velocityY)方法可以看到,它是从(0,0)坐标开始的。表示,不管当前的View移动到哪儿了,这里的Scroller计算的是,根据参数传递的速度,如果从(0,0)处开始,应该移动到哪儿了。

还有个ViewCompat.postOnAnimation(view, runable);这句等同于view.postDelayed(runable, 10);

4.3.3 插值器(Interpolator)

在初始化OverScroller的时候,用到了一个sQuinticInterpolator。具体的定义如下:

//f(x) = (x-1)^5 + 1
private static final Interpolator sQuinticInterpolator = new Interpolator() {@Overridepublic float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}
};

这里用的是一个自定义的插值器。上面提过一下,RecyclerView和ScrollView在惯性滑动上的一部分区别,RecyclerView用的是这个自定义的插值器,ScrollView用的是默认的Scroller.ViscousFluidInterpolator

插值器的直观的效果之一就是RecyclerView的惯性滑动。也就是刚开始很快,之后慢慢变慢直到停止的效果。

差值器的主要方法getInterpolation(float t)。参数t为滑行时间的百分比,从0到1。返回值为滑行距离的百分比,可以小于0,可以大于1。

用RecyclerView的插值器来举例。如果说根据手指抬起时的速度,最终需要5秒滑动1000像素。根据上面的sQuinticInterpolator插值器,在滑行了2秒的时候,t的值为2/5=0.4getInterpolation(0.4)=0.92表示已经滑动了0.92*1000=920个像素了。更直观的可以通过插值器在[0,1]上的曲线来表达:

横坐标表示时间,纵坐标表示已经完成了总路程的百分比。如图所示,RecyclerView的插值器形成的效果就是在很短时间内首先完成了大部分的路程。正式我们看到的前期很快,后来很慢的效果。

也附上Scroller.ViscousFluidInterpolator在[0,1]上的曲线图:

感觉没什么太大的区别。

4.4 边缘处理

滑动到上下两边的时候还是能滑动,不妥,需要进行约束。直接贴一下代码就好:

private void constrainScrollBy(int dx, int dy) {Rect viewport = new Rect();getGlobalVisibleRect(viewport);int height = viewport.height();int width = viewport.width();int scrollX = getScrollX();int scrollY = getScrollY();//右边界if (mWidth - scrollX - dx < width) {dx = mWidth - scrollX - width;}//左边界if (-scrollX - dx > 0) {dx = -scrollX;}//下边界if (mHeight - scrollY - dy < height) {dy = mHeight - scrollY - height;}//上边界if (scrollY + dy < 0) {dy = -scrollY;}scrollBy(dx, dy);
}

将代码中的scrollBy都改成添加约束的constrainScrollBy()即可。

五、 给出自定义View的源码

package com.rajesh.scrolldemo;import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.OverScroller;
import android.widget.TextView;/*** Created by zhufeng on 2017/7/26.*/public class CustomScrollView extends ViewGroup {private Context mContext;private int SCREEN_WIDTH = 0;private int SCREEN_HEIGHT = 0;private int mWidth = 0;private int mHeight = 0;private static final int INVALID_POINTER = -1;public static final int SCROLL_STATE_IDLE = 0;public static final int SCROLL_STATE_DRAGGING = 1;public static final int SCROLL_STATE_SETTLING = 2;private int mScrollState = SCROLL_STATE_IDLE;private int mScrollPointerId = INVALID_POINTER;private VelocityTracker mVelocityTracker;private int mLastTouchY;private int mTouchSlop;private int mMinFlingVelocity;private int mMaxFlingVelocity;private final ViewFlinger mViewFlinger = new ViewFlinger();//f(x) = (x-1)^5 + 1private static final Interpolator sQuinticInterpolator = new Interpolator() {@Overridepublic float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}};public CustomScrollView(Context context) {this(context, null);}public CustomScrollView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int top = 0;for (int i = 0; i < 20; i++) {int width = SCREEN_WIDTH;int height = SCREEN_HEIGHT / 2;int left = 0;int right = left + width;int bottom = top + height;//撑大边界if (bottom > mHeight) {mHeight = bottom;}if (right > mWidth) {mWidth = right;}TextView textView = new TextView(mContext);if (i % 2 == 0) {textView.setBackgroundColor(Color.CYAN);} else {textView.setBackgroundColor(Color.GREEN);}textView.setText("item:" + i);addView(textView);textView.layout(left, top, right, bottom);top += height;top += 20;}}private void init(Context context) {this.mContext = context;final ViewConfiguration vc = ViewConfiguration.get(context);mTouchSlop = vc.getScaledTouchSlop();mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();DisplayMetrics metric = context.getResources().getDisplayMetrics();SCREEN_WIDTH = metric.widthPixels;SCREEN_HEIGHT = metric.heightPixels;}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}boolean eventAddedToVelocityTracker = false;final int action = MotionEventCompat.getActionMasked(event);final int actionIndex = MotionEventCompat.getActionIndex(event);final MotionEvent vtev = MotionEvent.obtain(event);switch (action) {case MotionEvent.ACTION_DOWN: {setScrollState(SCROLL_STATE_IDLE);mScrollPointerId = event.getPointerId(0);mLastTouchY = (int) (event.getY() + 0.5f);break;}case MotionEventCompat.ACTION_POINTER_DOWN: {mScrollPointerId = event.getPointerId(actionIndex);mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);break;}case MotionEvent.ACTION_MOVE: {final int index = event.findPointerIndex(mScrollPointerId);if (index < 0) {Log.e("zhufeng", "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?");return false;}final int y = (int) (event.getY(index) + 0.5f);int dy = mLastTouchY - y;if (mScrollState != SCROLL_STATE_DRAGGING) {boolean startScroll = false;if (Math.abs(dy) > mTouchSlop) {if (dy > 0) {dy -= mTouchSlop;} else {dy += mTouchSlop;}startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}if (mScrollState == SCROLL_STATE_DRAGGING) {mLastTouchY = y;constrainScrollBy(0, dy);}break;}case MotionEventCompat.ACTION_POINTER_UP: {if (event.getPointerId(actionIndex) == mScrollPointerId) {// Pick a new pointer to pick up the slack.final int newIndex = actionIndex == 0 ? 1 : 0;mScrollPointerId = event.getPointerId(newIndex);mLastTouchY = (int) (event.getY(newIndex) + 0.5f);}break;}case MotionEvent.ACTION_UP: {mVelocityTracker.addMovement(vtev);eventAddedToVelocityTracker = true;mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);float yVelocity = -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId);Log.i("zhufeng", "速度取值:" + yVelocity);if (Math.abs(yVelocity) < mMinFlingVelocity) {yVelocity = 0F;} else {yVelocity = Math.max(-mMaxFlingVelocity, Math.min(yVelocity, mMaxFlingVelocity));}if (yVelocity != 0) {mViewFlinger.fling((int) yVelocity);} else {setScrollState(SCROLL_STATE_IDLE);}resetTouch();break;}case MotionEvent.ACTION_CANCEL: {resetTouch();break;}}if (!eventAddedToVelocityTracker) {mVelocityTracker.addMovement(vtev);}vtev.recycle();return true;}private void resetTouch() {if (mVelocityTracker != null) {mVelocityTracker.clear();}}private void setScrollState(int state) {if (state == mScrollState) {return;}mScrollState = state;if (state != SCROLL_STATE_SETTLING) {mViewFlinger.stop();}}private class ViewFlinger implements Runnable {private int mLastFlingY = 0;private OverScroller mScroller;private boolean mEatRunOnAnimationRequest = false;private boolean mReSchedulePostAnimationCallback = false;public ViewFlinger() {mScroller = new OverScroller(getContext(), sQuinticInterpolator);}@Overridepublic void run() {disableRunOnAnimationRequests();final OverScroller scroller = mScroller;if (scroller.computeScrollOffset()) {final int y = scroller.getCurrY();int dy = y - mLastFlingY;mLastFlingY = y;constrainScrollBy(0, dy);postOnAnimation();}enableRunOnAnimationRequests();}public void fling(int velocityY) {mLastFlingY = 0;setScrollState(SCROLL_STATE_SETTLING);mScroller.fling(0, 0, 0, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);postOnAnimation();}public void stop() {removeCallbacks(this);mScroller.abortAnimation();}private void disableRunOnAnimationRequests() {mReSchedulePostAnimationCallback = false;mEatRunOnAnimationRequest = true;}private void enableRunOnAnimationRequests() {mEatRunOnAnimationRequest = false;if (mReSchedulePostAnimationCallback) {postOnAnimation();}}void postOnAnimation() {if (mEatRunOnAnimationRequest) {mReSchedulePostAnimationCallback = true;} else {removeCallbacks(this);ViewCompat.postOnAnimation(CustomScrollView.this, this);}}}private void constrainScrollBy(int dx, int dy) {Rect viewport = new Rect();getGlobalVisibleRect(viewport);int height = viewport.height();int width = viewport.width();int scrollX = getScrollX();int scrollY = getScrollY();//右边界if (mWidth - scrollX - dx < width) {dx = mWidth - scrollX - width;}//左边界if (-scrollX - dx > 0) {dx = -scrollX;}//下边界if (mHeight - scrollY - dy < height) {dy = mHeight - scrollY - height;}//上边界if (scrollY + dy < 0) {dy = -scrollY;}scrollBy(dx, dy);}
}

android自定义控件的惯性滑动相关推荐

  1. Android自定义控件(一) 可滑动的进度条

    前言 本篇文章记录通过自定义View实现Android下可滑动的进度条 学习巩固自定义View知识 说明 1.实现效果 文中实现的效果都是未加抗锯齿 2.View绘制解析 上图自定义View中有文本( ...

  2. android汽车之家顶部滑动菜单,Android自定义控件之仿汽车之家下拉刷新

    关于下拉刷新的实现原理我在上篇文章Android自定义控件之仿美团下拉刷新中已经详细介绍过了,这篇文章主要介绍表盘的动画实现原理 汽车之家的下拉刷新分为三个状态: 第一个状态为下拉刷新状态(pull ...

  3. Android自定义控件:打造自己的QQ空间主页

    前面已经实现过仿QQ的List抽屉效果以及仿QQ未读消息拖拽效果,具体请见: Android自定义控件:类QQ抽屉效果 Android自定义控件:类QQ未读消息拖拽效果 趁热打铁,这次我们实现QQ空间 ...

  4. Android自定义控件:从零开始实现魅族flyme6应用市场应用详情弹出式layout

    前几天无意中发现魅族flyme6应用市场的应用详情界面非常有意思,作为一枚程序员,看到有意思的东西怎么办?当然是想办法自己也整一个啦,哈哈. 废话不多说,下面先看看魅族flyme6应用市场详情页弹出时 ...

  5. Android 自定义控件打造史上最简单的侧滑菜单

    侧滑菜单在很多应用中都会见到,最近QQ5.0侧滑还玩了点花样~~对于侧滑菜单,一般大家都会自定义ViewGroup,然后隐藏菜单栏,当手指滑动时,通过Scroller或者不断的改变leftMargin ...

  6. Android 自定义控件 按钮滚动选择

    效果图 代码实现 package com.demo.ui.view;import android.annotation.TargetApi; import android.content.Contex ...

  7. android listview ontouchlistener,Android ListView监听滑动事件的方法(详解)

    ListView的主要有两种滑动事件监听方法,OnTouchListener和OnScrollListener 1.OnTouchListener OnTouchListener方法来自View中的监 ...

  8. Android自定义控件:NestedScrolling实现仿魅族flyme6应用市场应用详情弹出式layout

    在前一篇博文中已经实现过一个仿魅族flyme6应用市场应用详情弹出式layout: Android自定义控件:从零开始实现魅族flyme6应用市场应用详情弹出式layout,主要是通过viewDrag ...

  9. Android自定义控件(二) Android下聚光灯实现

    前言 本篇文章记录Android下实现聚光灯功能,结合上篇文章[Android自定义控件(一) 可滑动的进度条]中进度条控件修改聚光灯的大小和背景透明度. 学习巩固自定义控件知识 说明 1.实现效果 ...

最新文章

  1. python 类方法 静态方法_python中类方法、类实例方法、静态方法的使用与区别
  2. python语言命名规定首字符不能是_python标识符命名规范原理解析
  3. 【杂谈】万万没想到,有三还有个保密的‘朋友圈’,那里面都在弄啥!
  4. [No0000130]WPF 4.5使用标记扩展订阅事件
  5. 【Linux】一步一步学Linux——paste命令(58)
  6. mysql权限日志_mysql权限管理、日志管理及常用工具
  7. 从前序与中序遍历序列构造二叉树
  8. 如何教女朋友学 Python?
  9. linux文件控制驱动程序,Linux设备驱动程序学习(6)-高级字符驱动程序操作[(3)设备文件的访问控制]...
  10. FPGA图像处理之边缘检测,中值滤波,图像均衡1。
  11. 从零开始学 Web 之 jQuery(二)获取和操作元素的属性
  12. LibreELEC(kodi)基本设置
  13. pytroch冻结某些层的常用方法
  14. unittest控制case执行顺序
  15. win10 android ios,一机多用?Win10可运行安卓和iOS应用
  16. 爱好数学的国王 C++
  17. [导入]梦幻快车(DreamMail) v4.0 正式版 ?
  18. 深度学习工作站搭建全过程
  19. 思寒漫谈测试人职业发展
  20. Avanci许可平台新增SK电讯、华硕电脑和大唐移动三家专利权人

热门文章

  1. java中的pv操作,PV操作简单理解
  2. Jupyter-Notebook笔记-01 安装与简单操作
  3. 英雄联盟无法启动 因计算机中,发生了未知的dx错误,英雄联盟无法启动
  4. 【科普】华为支付保护中心有什么用?
  5. 热乎乎的面试经验(java后端开发-5k-天津)
  6. php taint扩展,利用PHP扩展Taint找出网站的潜在安全漏洞实践
  7. 示例教程:在C#中将MS Visio图表转换为PDF
  8. java修饰符总结,关于Java中修饰符的总结(fina除外)
  9. CSV和XLSX文件格式的区别
  10. 【SpringBoot】33、SpringBoot+LayUI后台管理系统开发脚手架