简述RecyclerView的fling过程

2023-10-29

我们以RecyclerView为例,研究一下ListView是怎么滑动并且更新view的。
首先可以肯定的是以Choreographer为基础实现的。

一、fling过程研究

fling动作是由input事件触发的。

1.1 RecyclerView.onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
            return false;
        }
        // 给子view一个拦截处理input事件的机会
        if (dispatchOnItemTouch(e)) {
            cancelTouch();
            return true;
        }

        if (mLayout == null) {
            return false;
        }

        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;

        final MotionEvent vtev = MotionEvent.obtain(e);
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            ......
            case MotionEvent.ACTION_UP: {
                // 计算速度
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                // 速度大于0,就可以执行fling了
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;
            ......
        }
        ......
        return true;
    }

1.2 RecyclerView.fling

    public boolean fling(int velocityX, int velocityY) {
        if (mLayout == null) {
            Log.e(TAG, "Cannot fling without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return false;
        }
        if (mLayoutFrozen) {
            return false;
        }

        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
        final boolean canScrollVertical = mLayout.canScrollVertically();

        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            // If we don't have any velocity, return false
            return false;
        }

        // 先将这个fling事件分发给嵌套的滚动父视图
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {
                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                // 如果条件都符合,开始fling
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

1.3 RecyclerView.ViewFlinger.fling

        public void fling(int velocityX, int velocityY) {
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            // 1.4 使用OverScroller处理每一帧的速度和下一帧的距离
            mScroller.fling(0, 0, velocityX, velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            // 1.5 通过Choreographer实现动画
            postOnAnimation();
        }

说明一下,ViewFlinger是一个实现Runnable的类.

1.4 OverScroller.fling

    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {
        fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
    }

    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY, int overX, int overY) {
        // Continue a scroll or fling in progress
        if (mFlywheel && !isFinished()) {
            float oldVelocityX = mScrollerX.mCurrVelocity;
            float oldVelocityY = mScrollerY.mCurrVelocity;
            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }
        // 设置模式为FLING_MODE
        mMode = FLING_MODE;
        // 分别对X轴和Y轴做fling, mScrollerX和mScrollerY都是SplineOverScroller类型
        // 当然常见的是以Y轴的滚动。这里我们看Y轴的处理
        mScrollerX.fling(startX, velocityX, minX, maxX, overX);
        mScrollerY.fling(startY, velocityY, minY, maxY, overY);
    }

1.4.1 SplineOverScroller.fling

        void fling(int start, int velocity, int min, int max, int over) {
            mOver = over;
            mFinished = false;
            mCurrVelocity = mVelocity = velocity;
            mDuration = mSplineDuration = 0;
            mStartTime = AnimationUtils.currentAnimationTimeMillis();
            mCurrentPosition = mStart = start;

            if (start > max || start < min) {
                startAfterEdge(start, min, max, velocity);
                return;
            }

            // 注意这里将状态设置为SPLINE,表示是样条插值
            mState = SPLINE;
            double totalDistance = 0.0;
            // 根据初速度计算fling的时长和距离
            if (velocity != 0) {
                mDuration = mSplineDuration = getSplineFlingDuration(velocity);
                totalDistance = getSplineFlingDistance(velocity);
            }

            mSplineDistance = (int) (totalDistance * Math.signum(velocity));
            mFinal = start + mSplineDistance;

            // 越界处理
            if (mFinal < min) {
                adjustDuration(mStart, mFinal, min);
                mFinal = min;
            }

            if (mFinal > max) {
                adjustDuration(mStart, mFinal, max);
                mFinal = max;
            }
        }

1.5 RecyclerView.ViewFlinger.postOnAnimation

        void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                // this就是指ViewFlinger
                removeCallbacks(this);
                // 最终就是调用父类View的方法
                RecyclerView.this.postOnAnimation(this);
            }
        }

1.5.1 View.postOnAnimation

    public void postOnAnimation(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            // 这里就是使用Choreographer实现回调的,也就是下一帧
            // 会回调ViewFlinger中的run方法,而且这里回调也是Animation类型
            attachInfo.mViewRootImpl.mChoreographer.postCallback(
                    Choreographer.CALLBACK_ANIMATION, action, null);
        } else {
            // Postpone the runnable until we know
            // on which thread it needs to run.
            getRunQueue().post(action);
        }
    }

接下来,看下一帧的处理

1.6 ViewFlinger.run

        @Override
        public void run() {
            if (mLayout == null) {
                stop();
                return; // no layout, cannot scroll.
            }
            disableRunOnAnimationRequests();
            consumePendingUpdateOperations();
            // keep a local reference so that if it is changed during onAnimation method, it won't
            // cause unexpected behaviors
            final OverScroller scroller = mScroller;
            final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
            // 1.6.1 计算OverScroller的滚动,滚动结束返回false
            if (scroller.computeScrollOffset()) {
                // 根据滚动距离计算应该显示的item
                final int x = scroller.getCurrX();
                final int y = scroller.getCurrY();
                final int dx = x - mLastFlingX;
                final int dy = y - mLastFlingY;
                int hresult = 0;
                int vresult = 0;
                mLastFlingX = x;
                mLastFlingY = y;
                int overscrollX = 0, overscrollY = 0;
                if (mAdapter != null) {
                    eatRequestLayout();
                    onEnterLayoutOrScroll();
                    Trace.beginSection(TRACE_SCROLL_TAG);
                    if (dx != 0) {
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }
                    Trace.endSection();
                    repositionShadowingViews();

                    onExitLayoutOrScroll();
                    resumeRequestLayout(false);

                    if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
                            && smoothScroller.isRunning()) {
                        final int adapterSize = mState.getItemCount();
                        if (adapterSize == 0) {
                            smoothScroller.stop();
                        } else if (smoothScroller.getTargetPosition() >= adapterSize) {
                            smoothScroller.setTargetPosition(adapterSize - 1);
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        } else {
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        }
                    }
                }
                if (!mItemDecorations.isEmpty()) {
                    // 有内容则请求绘制
                    invalidate();
                }
                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
                    considerReleasingGlowsOnScroll(dx, dy);
                }
                if (overscrollX != 0 || overscrollY != 0) {
                    final int vel = (int) scroller.getCurrVelocity();

                    int velX = 0;
                    if (overscrollX != x) {
                        velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
                    }

                    int velY = 0;
                    if (overscrollY != y) {
                        velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
                    }

                    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
                        absorbGlows(velX, velY);
                    }
                    if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
                            && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
                        scroller.abortAnimation();
                    }
                }
                if (hresult != 0 || vresult != 0) {
                    // 分发滚动事件
                    dispatchOnScrolled(hresult, vresult);
                }
                // 这个是绘制ScollBar,也就是滚动条
                if (!awakenScrollBars()) {
                    invalidate();
                }

                final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
                        && vresult == dy;
                final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
                        && hresult == dx;
                final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
                        || fullyConsumedVertical;

                // 判断滚动是否结束
                if (scroller.isFinished() || !fullyConsumedAny) {
                    setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
                    if (ALLOW_THREAD_GAP_WORK) {
                        mPrefetchRegistry.clearPrefetchPositions();
                    }
                } else {
                    // 未结束,发送回调,在下一个Vsync时再来一轮,直到滚动结束
                    postOnAnimation();
                    if (mGapWorker != null) {
                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
                    }
                }
            }
            // call this after the onAnimation is complete not to have inconsistent callbacks etc.
            if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            }
            enableRunOnAnimationRequests();
        }

1.6.1 OverScroller.computeScrollOffset

    public boolean computeScrollOffset() {
        if (isFinished()) {
            return false;
        }
        // 我们在#1.4 OverScroller.fling中知道,此时是FLING_MODE中
        switch (mMode) {
            ......
            case FLING_MODE:
                if (!mScrollerX.mFinished) {
                    if (!mScrollerX.update()) {
                        if (!mScrollerX.continueWhenFinished()) {
                            mScrollerX.finish();
                        }
                    }
                }
                // 判断是否已经结束
                if (!mScrollerY.mFinished) {
                    // #1.6.1.1 处理更新,返回值表明fling是否已结束, false为已结束
                    // 一般正在fling中是返回true的
                    if (!mScrollerY.update()) {
                        // #1.6.1.2 处理fling的结束,结束返回false
                        if (!mScrollerY.continueWhenFinished()) {
                            // 结束fling,就是标记当前位置为mFinal,且将mFinished标记为true
                            mScrollerY.finish();
                        }
                    }
                }

                break;
        }

        return true;
    }
1.6.1.1 SplineOverScroller.update
        boolean update() {
            final long time = AnimationUtils.currentAnimationTimeMillis();
            final long currentTime = time - mStartTime;

            if (currentTime == 0) {
                // Skip work but report that we're still going if we have a nonzero duration.
                return mDuration > 0;
            }
            if (currentTime > mDuration) {
                return false;
            }

            double distance = 0.0;
            switch (mState) {
                case SPLINE: {
                    // 重点看这里的算法,不过在看这个算法之前,我们先看看
                    // 这个所谓的样条曲线是什么样的,以及SPLINE_POSITION数组
                    // 是如何生成的。见 #2.1 SplineOverScroller的初始化
                    final float t = (float) currentTime / mSplineDuration;
                    final int index = (int) (NB_SAMPLES * t);
                    float distanceCoef = 1.f;
                    float velocityCoef = 0.f;
                    if (index < NB_SAMPLES) {
                        final float t_inf = (float) index / NB_SAMPLES;
                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
                        final float d_inf = SPLINE_POSITION[index];
                        final float d_sup = SPLINE_POSITION[index + 1];
                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                    }

                    distance = distanceCoef * mSplineDistance;
                    mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
                    break;
                }
                ......
            }

            mCurrentPosition = mStart + (int) Math.round(distance);

            return true;
        }

这里我们暂时不看算法内容,在第二节中详细研究。
只要知道这里根据样条插值更新了当前时间对应的距离位置即可。

1.6.1.2 SplineOverScroller.continueWhenFinished
        boolean continueWhenFinished() {
            switch (mState) {
                case SPLINE:
                    // 正常来讲,mDuration 是等于 mSplineDuration 的
                    // 如果小于,则说明达到ListView的边界了,需要调整各个参数
                    if (mDuration < mSplineDuration) {
                        // If the animation was clamped, we reached the edge
                        mCurrentPosition = mStart = mFinal;
                        // TODO Better compute speed when edge was reached
                        mVelocity = (int) mCurrVelocity;
                        mDeceleration = getDeceleration(mVelocity);
                        mStartTime += mDuration;
                        onEdgeReached();
                    } else {
                        // 正常结束
                        return false;
                    }
                    break;
                    ......
            }
            update();
            return true;
        }

至此,ListView的滚动流程已经结束。总而言之,就是利用Choreographer的animation回调进行逐帧处理。
接下来,我们研究下传说中的滑动曲线。

二. 样条曲线的核心算法

我们知道,Android中ListView的滑动曲线是一条平滑的曲线,使用的是样条插值方式实现。
样条值是存贮在SplineOverScroller中的, 这个类是一个静态类。

2.1 SplineOverScoller的初始化

在静态区有如下的初始化代码块,就是用来初始化样条曲线的插值的

        static {
            float x_min = 0.0f;
            float y_min = 0.0f;
            for (int i = 0; i < NB_SAMPLES; i++) {
                final float alpha = (float) i / NB_SAMPLES;

                float x_max = 1.0f;
                float x, tx, coef;
                while (true) {
                    x = x_min + (x_max - x_min) / 2.0f;
                    coef = 3.0f * x * (1.0f - x);
                    tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                    if (Math.abs(tx - alpha) < 1E-5) break;
                    if (tx > alpha) x_max = x;
                    else x_min = x;
                }
                SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

                float y_max = 1.0f;
                float y, dy;
                while (true) {
                    y = y_min + (y_max - y_min) / 2.0f;
                    coef = 3.0f * y * (1.0f - y);
                    dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                    if (Math.abs(dy - alpha) < 1E-5) break;
                    if (dy > alpha) y_max = y;
                    else y_min = y;
                }
                SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
            }
            SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
        }

算法的具体实现就不分析,我们看看最终的曲线呈现:

在这里插入图片描述

这里就有个疑问了,这里的样条插值只有NB_SAMPLES(100)个。那么这里是如何实现对不同的时长的
滑动也能使用同一条滑动曲线呢?我们回到计算当前帧下的滑动位置方法里,也就是SplineOverScroller.update中。

2.2 SplineOverScroller.update

        boolean update() {
            // 获取当前时间戳,计算已经滑动的时长
            final long time = AnimationUtils.currentAnimationTimeMillis();
            final long currentTime = time - mStartTime;
            ......
            double distance = 0.0;
            switch (mState) {
                case SPLINE: {
                    // 这个t就是当前滑动的时长所占总滑动时长的百分比
                    final float t = (float) currentTime / mSplineDuration;
                    // 注意这里的是int,也就是向下取整
                    final int index = (int) (NB_SAMPLES * t);
                    float distanceCoef = 1.f;
                    float velocityCoef = 0.f;
                    // 防止越界
                    if (index < NB_SAMPLES) {
                        // 计算当前时间对应的前后插值的index
                        final float t_inf = (float) index / NB_SAMPLES;
                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
                        // 拿到最近的前后插值
                        final float d_inf = SPLINE_POSITION[index];
                        final float d_sup = SPLINE_POSITION[index + 1];
                        // 根据比例算的当前时间对应的速度,然后算得当前的滑动距离
                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                    }

                    distance = distanceCoef * mSplineDistance;
                    mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
                    break;
                }
                ......
            }
            // 更新当前位置
            mCurrentPosition = mStart + (int) Math.round(distance);

            return true;
        }

哦吼,原来滑动曲线并不是真正的曲线,而是由99条线段组成的。
但是由于用户实际滑动的时间一般在1s~5s之间,在60Hz下并不影响体验效果。
如果是高帧率的情况,应该还是有调优的空间的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

简述RecyclerView的fling过程 的相关文章

随机推荐

  • C++ primer 【笔记】C++中this指针的用法详解

    1 this指针的用处 一个对象的this指针并不是对象本身的一部分 不会影响sizeof 对象 的结果 this作用域是在类内部 当在类的非静态成员函数中访问类的非静态成员的时候 编译器会自动将对象本身的地址作为一个隐含参数传递给函数 也
  • 【Linux命令详解

    文章标题 简介 一 参数列表 二 使用介绍 1 分页显示文件内容 2 搜索关键词 3 显示行号 4 显示特定内容 5 只显示匹配行 6 忽略大小写搜索 7 输出到文件 8 动态查看文件增长 9 开启对二进制文件的支持 10 显示控制字符 1
  • 博客搬家系列(六)-爬取今日头条文章

    博客搬家系列 六 爬取今日头条文章 一 前情回顾 博客搬家系列 一 简介 https blog csdn net rico zhou article details 83619152 博客搬家系列 二 爬取CSDN博客 https blog
  • 前端和后端就业前景如何?

    我个人的信息来源有两个渠道 一个是观察公司内网发布的招聘信息 另一个是观察朋友圈内猎头经常发布的招聘信息 基本算是从横向与纵向两个视角 较为全面的了解当前市场 先说结论 就国内市场而言 前端开发要求较容易 而发展前景相应的受限 发布的职位也
  • 数值作业:顺序消去法解线性方程组之C语言代码

    实际上后面的Guass列主选主元 全选主元 都是由顺序高斯消元法稍加改动变化而来的 但是顺序消元会出现一个问题 如果我们要保留的那个元的系数很小 那么在消元过程中 势必会用很大的数字乘以次方程后再加到别的方程上消去别的方程中的改元 这样就会
  • 《FFmpeg Basics》中文版-09-overlay-画中画

    正文 overlay视频技术经常被使用 常见的例子是放置在电视屏幕上的电视频道标志 通常位于右上角 以标识特定的频道 另一个例子是画中画功能 可以在主屏幕的其中一个角落显示小窗口 小窗口包含选定的电视频道或其他内容 同时在主屏幕上观看节目
  • Three.js基础介绍

    文章目录 前言 项目引入 项目介绍 推荐理由 场景展示 总结 前言 Three js是基于原生WebGL封装运行的三维引擎 在所有WebGL引擎中 Three js是国内文资料最多 使用最广泛的三维引擎 项目引入 Three js中文网 g
  • android 相机预览编译 libyuv 处理 YUV 数据

    下载源码 需翻墙 Android Studio 新建一个 NDK 项目 源码拷贝到 cpp 目录下 include 下面是头文件 source 下面是源码 其它文件基本用不到不用管 CMakeLists txt 是 cmake 编译脚本 现
  • IBM的语音识别(IBM speech to text 语言转换成文字)

    1 登陆网址https www ibm com watson developercloud speech to text html并注册 2 打开网址https console ng bluemix net catalog category
  • 前女友

    点击这个会出现代码 简而言之 要满足v1 v2 但是md5 v1 md5 v2 1 可以通过 PHP处理0e开头md5时hash字符串漏洞 0e开头md5所代表的值相同 来构造 下面这篇文章中有关于这个的构造 https blog csdn
  • 南航829数据结构历年真题答案

    2013年真题 第四题 问题描述 已知有两个带头结点的单链表A和B 元素值递增有序 编写函数调整删减链表 使A链表的元素值为A B的交集 并成为一个递减有序的单链表 要求写出算法思想以及相应代码 代码 2013数据结构第四题 include
  • 使用null,not in翻车了(oracle)

    水平有限 如有错误 请多指正 由于对null和not in理解得不是很透彻 导致在生产环境出问题了 请看下面的sql 为了简单 sql做过调整 select sd prestpword pres from ci pres pres wher
  • 全球第二大成人网站,黄了!

    推荐大家关注一个公众号 点击上方 编程技术圈 关注 星标或置顶一起成长 后台回复 大礼包 有惊喜礼包 每日英文 To give up is easy But to hold it together when everyone else th
  • 历届试题 高僧斗法  (博弈)

    题目 历届试题 高僧斗法 时间限制 1 0s 内存限制 256 0MB 锦囊1 博弈论 NIM取子游戏 锦囊2 将两个两个看成一组 他们之间的间隔可以看成一个NIM取子游戏 问题描述 古时丧葬活动中经常请高僧做法事 仪式结束后 有时会有 高
  • tomcat加载jar包顺序

    概述 项目使用springMVC serviceImpl注入的一个bean无法找到 究其原因是无法找到日志类 其实在spring的配置文件中配置了bean 而且程序代码在其他人的机子上运行不报错 我这边抱错 类找不到apache commo
  • 华为手机如何固定横屏_华为手机屏幕如何转为横屏?很简单,只需这样设置

    设置华为手机横屏显示 需要打开手机的 自动旋转 功能 在使用时将手机机身横置即可 以华为P20Pro为例 详细操作步骤如下 1 从屏幕顶部向下滑动 调出系统的通知面板 2 向下拖拽通知面板 让面板显示全部快捷功能 3 在通知面板中 找到并打
  • SQLyog快捷键,这一篇就够!!

    我们在使用SQLyog进操作时 如果不使用快捷键 会很麻烦 尤其是多行注释这种骚操作 所以在非常忙碌的工作中 使劲的挤了挤 挤出点时间 来整理一下sqlyog的常用快捷键骚操作 一 连接 Ctrl M 创建一个新的连接 Ctrl N 使用当
  • C# 参数传递(引用类型参数)

    目录 一 引言 二 引用类型参数作为值参数传递 三 引用类型参数作为引用参数传递 一 引言 方法中参数的传递方式主要有值参数传递和引用参数传递 ref out 而参数有可以分为值类型参数和引用类型参数 这里主要讲一讲引用类型参数的值 引用参
  • STM32F407IG单片机读写SD2405ALPI实时时钟程序(包括:读时钟时间、写时间到时钟、时间报警中断、倒计时中断)

    具体的IIC时序图和分析过程请参见下面网友的文章 https blog csdn net ybhuangfugui article details 52151835 本人在STM32F407单片机上亲测读时钟 写时钟 时间中断以及倒计时 秒
  • 简述RecyclerView的fling过程

    我们以RecyclerView为例 研究一下ListView是怎么滑动并且更新view的 首先可以肯定的是以Choreographer为基础实现的 一 fling过程研究 fling动作是由input事件触发的 1 1 RecyclerVi