Android 自定义万能的抽屉布局(侧滑菜单)GenericDrawerLayout

2023-10-27

转载请注明出处:
http://blog.csdn.net/a740169405/article/details/49720973

前言

大家应该对侧滑菜单很熟悉了,大多数是从左侧滑出。其实实现原理是v4支持包提供的一个类DrawerLayout。今天我要带大家自己定义一个DrawerLayout,并且支持从屏幕四个边缘划出来。GO~

先看看效果图:
效果图

一、布局

自定义的容器里,包含三个View,一个是用来绘制背景颜色的,第二个是在抽屉关闭时,用来响应触摸事件接着打开抽屉。另一个是用来存放抽屉视图的FrameLayout。
PS: 这里的三个View都是自定义View,为什么要自定义,后面会讲到。
我们看看初始化函数initView:

private void initView() {
    // 初始化背景色变化控件
    mDrawView = new DrawView(mContext);
    addView(mDrawView, generateDefaultLayoutParams());
    // 初始化用来相应触摸的透明View
    mTouchView = new TouchView(mContext);
    mClosedTouchViewSize = dip2px(mContext, TOUCH_VIEW_SIZE_DIP);
    mOpenedTouchViewSize = mClosedTouchViewSize;
    // 初始化用来存放布局的容器
    mContentLayout = new ContentLayout(mContext);
    mContentLayout.setVisibility(View.INVISIBLE);
    // 添加视图
    addView(mTouchView, generateTouchViewLayoutParams());
    addView(mContentLayout,
            new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    // 用来判断事件下发的临界距离
    mMinDisallowDispatch = dip2px(mContext, MIN_CONSUME_SIZE_DIP);
}

接着,既然要支持四个方向拉出抽屉,我设置了变量mTouchViewGravity 用来存放抽屉的相对屏幕的中心,其取值范围在系统类Gravity,的LEFT, TOP, RIGHT, BOTTOM里。

/* 当前抽屉的Gravity /
private int mTouchViewGravity = Gravity.LEFT;

抽屉默认为LEFT从左边拉出来。

提供一个接口,方便用户设置抽屉位置:

/**
 * 设置抽屉的位置
 *
 * @param drawerPosition 抽屉位置
 * @see Gravity
 */
public void setDrawerGravity(int drawerPosition) {
    if (drawerPosition != Gravity.LEFT && drawerPosition != Gravity.TOP
            && drawerPosition != Gravity.RIGHT && drawerPosition != Gravity.BOTTOM) {
        // 如果不是LEFT, TOP, RIGHT, BOTTOM中的一种,直接返回
        return;
    }
    this.mTouchViewGravity = drawerPosition;
    // 更新抽屉位置
    mTouchView.setLayoutParams(generateTouchViewLayoutParams());
}

二、Touch事件处理

在这里,我们需要拦截Touch事件的传递,我这里讲拦截动作放在Touch事件分发的时候处理。也就是dispatchTouchEvent(MotionEvent event);函数里。
讲到这,大家应该明白了为什么我要自定义View来拦截事件。通过重写dispatchTouchEvent函数来获取事件,并根据当前情况判断是否需要将事件继续下发。

1. 拉出抽屉

直接上代码:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!mIsOpenable) {
        // 如果禁用了抽屉
        return super.dispatchTouchEvent(event);
    }
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        if (getVisibility() == View.INVISIBLE) {
            // 如果当前TouchView不可见
            return super.dispatchTouchEvent(event);
        }
        // 显示抽屉
        mContentLayout.setVisibility(View.VISIBLE);
        // 调整抽屉位置
        adjustContentLayout();
        if (mDrawerCallback != null) {
            // 回调事件(开始打开抽屉)
            mDrawerCallback.onPreOpen();
        }
        // 隐藏TouchView
        setVisibility(View.INVISIBLE);
        break;
    }
    // 处理Touch事件
    performDispatchTouchEvent(event);
    return true;
}

当TouchDown时,需要调整抽屉的位置:

/**
 * 拖拽开始前,调整内容视图位置
 */
private void adjustContentLayout() {
    float mStartTranslationX = 0;
    float mStartTranslationY = 0;
    switch (mTouchViewGravity) {
    case Gravity.LEFT:
        mStartTranslationX = -mContentLayout.getWidth();
        mStartTranslationY = 0;
        break;
    case Gravity.RIGHT:
        mStartTranslationX = mContentLayout.getWidth();
        mStartTranslationY = 0;
        break;
    case Gravity.TOP:
        mStartTranslationX = 0;
        mStartTranslationY = -mContentLayout.getHeight();
        break;
    case Gravity.BOTTOM:
        mStartTranslationX = 0;
        mStartTranslationY = mContentLayout.getHeight();
        break;
    }
    // 移动抽屉
    ViewHelper.setTranslationX(mContentLayout, mStartTranslationX);
    ViewHelper.setTranslationY(mContentLayout, mStartTranslationY);
}

我们看看是怎么处理Touch事件分发的:

private void performDispatchTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
        // 速度测量
        mVelocityTracker = VelocityTracker.obtain();
    }
    // 调整事件信息,用于测量速度
    MotionEvent trackerEvent = MotionEvent.obtain(event);
    trackerEvent.setLocation(event.getRawX(), event.getRawY());
    mVelocityTracker.addMovement(trackerEvent);
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        // 记录当前触摸的位置
        mCurTouchX = event.getRawX();
        mCurTouchY = event.getRawY();
        break;
    case MotionEvent.ACTION_MOVE:
        float moveX = event.getRawX() - mCurTouchX;
        float moveY = event.getRawY() - mCurTouchY;
        // 移动抽屉
        translateContentLayout(moveX, moveY);
        mCurTouchX = event.getRawX();
        mCurTouchY = event.getRawY();
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
        // 处理抬起事件
        handleTouchUp();
        break;
    }
}

我们用到了VelocityTracker来测量手指滑动的速度,需要注意的是,VelocityTracker是通过MotionEvent的getX();以及getY();获取当前X、Y轴的值,因为这两个值是相对父容器的位置,这里我把Touch事件的X,Y值调整为相对屏幕左上角的X,Y轴值,这样能够获取精确的速度值。

接着,需要实现拖拽效果,这里借用了NineOldAndroids开源库,能够在android 2.x的固件上实现移动效果。接着看看如何处理的:

/**
 * 移动视图
 *
 * @param moveX
 * @param moveY
 */
private void translateContentLayout(float moveX, float moveY) {
    float move;
    switch (mTouchViewGravity) {
        case Gravity.LEFT:
            if (getCurTranslation() + moveX < -mContentLayout.getWidth()) {
                // 完全关闭
                move = -mContentLayout.getWidth();
            } else if (getCurTranslation() + moveX > 0) {
                // 完全打开
                move = 0;
            } else {
                move = getCurTranslation() + moveX;
            }
            break;
        case Gravity.RIGHT:
            if (getCurTranslation() + moveX > mContentLayout.getWidth()) {
                move = mContentLayout.getWidth();
            } else if (getCurTranslation() + moveX< 0) {
                move = 0;
            } else {
                move = getCurTranslation() + moveX;
            }
            break;
        case Gravity.TOP:
            if (getCurTranslation() + moveY < -mContentLayout.getHeight()) {
                move = -mContentLayout.getHeight();
            } else if (getCurTranslation() + moveY > 0) {
                move = 0;
            } else {
                move = getCurTranslation() + moveY;
            }
            break;
        case Gravity.BOTTOM:
            if (getCurTranslation() + moveY > mContentLayout.getHeight()) {
                move = mContentLayout.getHeight();
            } else if (getCurTranslation() + moveY < 0) {
                move = 0;
            } else {
                move = getCurTranslation() + moveY;
            }
            break;
        default:
            move = 0;
            break;
    }
    if (isHorizontalGravity()) {
        // 使用兼容低版本的方法移动抽屉
        ViewHelper.setTranslationX(mContentLayout, move);
        // 回调事件
        translationCallback(mContentLayout.getWidth() - Math.abs(move));
    } else {
        // 使用兼容低版本的方法移动抽屉
        ViewHelper.setTranslationY(mContentLayout, move);
        // 回调事件
        translationCallback(mContentLayout.getHeight() - Math.abs(move));
    }
}

这个函数里主要是根据当前移动的距离调整抽屉的位置。

当手指放开的时候,需要处理TouchUp事件:

private void handleTouchUp() {
    // 计算从Down到Up每秒移动的距离
    final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000);
    int velocityX = (int) velocityTracker.getXVelocity();
    int velocityY = (int) velocityTracker.getYVelocity();

    // 回收测量器
    if (mVelocityTracker != null) {
        mVelocityTracker.recycle();
        mVelocityTracker = null;
    }

    switch (mTouchViewGravity) {
    case Gravity.LEFT:
        if (velocityX > VEL || (getCurTranslation() > -mContentLayout.getWidth() * SCALE_AUTO_OPEN_CLOSE) && velocityX > -VEL) {
            // 速度足够,或者移动距离足够,打开抽屉
            autoOpenDrawer();
        } else {
            autoCloseDrawer();
        }
        break;
    case Gravity.RIGHT:
        if (velocityX < -VEL || (getCurTranslation() < mContentLayout.getWidth() * (1 - SCALE_AUTO_OPEN_CLOSE) && velocityX < VEL)) {
            // 速度足够,或者移动距离足够,打开抽屉
            autoOpenDrawer();
        } else {
            autoCloseDrawer();
        }
        break;
    case Gravity.TOP:
        if (velocityY > VEL || (getCurTranslation() > -mContentLayout.getHeight() * SCALE_AUTO_OPEN_CLOSE) && velocityY > -VEL) {
            // 速度足够,或者移动距离足够,打开抽屉
            autoOpenDrawer();
        } else {
            autoCloseDrawer();
        }
        break;
    case Gravity.BOTTOM:
        if (velocityY < -VEL || (getCurTranslation() < mContentLayout.getHeight() * (1 - SCALE_AUTO_OPEN_CLOSE)) && velocityY < VEL) {
            // 速度足够,或者移动距离足够,打开抽屉
            autoOpenDrawer();
        } else {
            autoCloseDrawer();
        }
        break;
    }
}

根据放手的时候的速度大小是否满足最小速度要求,以及滑动的距离是否满足最小要求,判断当前是要打开还是关闭。

抽屉的打开与关闭,需要平缓的过度,需要做一个过度动画,这里同样是使用nineoldandroids实现的:

/**
 * 自动打开抽屉
 */
private void autoOpenDrawer() {
    mAnimating.set(true);
    // 从当前移动的位置,平缓移动到完全打开抽屉的位置
    mAnimator = ObjectAnimator.ofFloat(getCurTranslation(), getOpenTranslation());
    mAnimator.setDuration(DURATION_OPEN_CLOSE);
    mAnimator.addUpdateListener(new MyAnimatorUpdateListener());
    mAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            // 回掉事件
            if (!AnimStatus.OPENING.equals(mAnimStatus) && !AnimStatus.OPENED.equals(mAnimStatus)) {
                if (mDrawerCallback != null) {
                    mDrawerCallback.onStartOpen();
                }
            }
            // 更新状态
            mAnimStatus = AnimStatus.OPENING;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (!mAnimating.get()) {
                // 正在播放动画(打开/关闭)
                return;
            }
            if (mDrawerCallback != null) {
                mDrawerCallback.onEndOpen();
            }
            mAnimating.set(false);
            mAnimStatus = AnimStatus.OPENED;
        }
    });
    mAnimator.start();
}
/**
 * 自动关闭抽屉
 */
private void autoCloseDrawer() {
    mAnimating.set(true);
    mAnimator = ObjectAnimator.ofFloat(getCurTranslation(), getCloseTranslation());
    mAnimator.setDuration(DURATION_OPEN_CLOSE);
    mAnimator.addUpdateListener(new MyAnimatorUpdateListener());
    mAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            if (!AnimStatus.CLOSING.equals(mAnimStatus) && !AnimStatus.CLOSED.equals(mAnimStatus)) {
                if (mDrawerCallback != null) {
                    mDrawerCallback.onStartClose();
                }
            }
            mAnimStatus = AnimStatus.CLOSING;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (!mAnimating.get()) {
                return;
            }
            if (mDrawerCallback != null) {
                mDrawerCallback.onEndClose();
                mAnimStatus = AnimStatus.CLOSED;
            }
            // 当抽屉完全关闭的时候,将响应打开事件的View显示
            mTouchView.setVisibility(View.VISIBLE);
            mAnimating.set(false);
        }
    });
    mAnimator.start();
}

这样,打开抽屉的做完了。接着要实现关闭的过程,这个过程相对比较复杂,因为触摸事件需要分发给抽屉里的视图,情况比较多,我们还是从抽屉容器的dispatchTouchEvent方法入手。

/**
 * 抽屉容器
 */
private class ContentLayout extends FrameLayout {

    private float mDownX, mDownY;
    private boolean isTouchDown;

    public ContentLayout(Context context) {
        super(context);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (getVisibility() != View.VISIBLE) {
            // 抽屉不可见
            return super.dispatchTouchEvent(event);
        }

        // TOUCH_DOWN的时候未消化事件
        if (MotionEvent.ACTION_DOWN != event.getAction() && !isTouchDown) {
            isChildConsumeTouchEvent = true;
        }

        // 把事件拦截下来,按条件下发给子View;
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (mAnimating.get()) {
                mAnimating.set(false);
                // 停止播放动画
                mAnimator.end();
                isTouchDown = true;
            } else {
                // 判断是否点击在响应区域内
                isTouchDown = isDownInRespondArea(event);
            }
            if (isTouchDown) {
                mDownX = event.getRawX();
                mDownY = event.getRawY();
                performDispatchTouchEvent(event);
            } else {
                // 标记为子视图消费事件
                isChildConsumeTouchEvent = true;
            }
            // 传递给子视图
            super.dispatchTouchEvent(event);
            // 拦截事件
            return true;
        case MotionEvent.ACTION_MOVE:
            if (!isConsumeTouchEvent && !isChildConsumeTouchEvent) {

                // 先下发给子View看看子View是否需要消费
                boolean b = super.dispatchTouchEvent(event);

                // 如果自己还没消化掉事件,看看子view是否需要消费事件
                boolean goToConsumeTouchEvent = false;
                switch (mTouchViewGravity) {
                    case Gravity.LEFT:
                        if ((Math.abs(event.getRawY() - mDownY) >= mMinDisallowDispatch) && b) {
                            // 当抽屉在左侧,手指在Y轴移动的距离大于临界值,并且子视图消费了Move事件,则标记为子视图已经消费
                            isChildConsumeTouchEvent = true;
                        } else if (event.getRawX() - mDownX < -mMinDisallowDispatch) {
                            // 当X轴方向移动的距离大于临界值的时候,标记为抽屉消费了事件,这时候需要移动抽屉
                            isConsumeTouchEvent = true;
                            goToConsumeTouchEvent = true;
                        }
                        break;
                    case Gravity.RIGHT:
                        if ((Math.abs(event.getRawY() - mDownY) >= mMinDisallowDispatch) && b) {
                            // 当抽屉在右侧,手指在Y轴移动的距离大于临界值,并且子视图消费了Move事件,则标记为子视图已经消费
                            isChildConsumeTouchEvent = true;
                        } else if (event.getRawX() - mDownX > mMinDisallowDispatch) {
                            // 当X轴方向移动的距离大于临界值的时候,标记为抽屉消费了事件,这时候需要移动抽屉
                            isConsumeTouchEvent = true;
                            goToConsumeTouchEvent = true;
                        }
                        break;
                    case Gravity.BOTTOM:
                        if ((Math.abs(event.getRawX() - mDownX) >= mMinDisallowDispatch) && b) {
                            // 当抽屉在下侧,手指在X轴移动的距离大于临界值,并且子视图消费了Move事件,则标记为子视图已经消费
                            isChildConsumeTouchEvent = true;
                        } else if (event.getRawY() - mDownY > mMinDisallowDispatch) {
                            // 当Y轴方向移动的距离大于临界值的时候,标记为抽屉消费了事件,这时候需要移动抽屉
                            isConsumeTouchEvent = true;
                            goToConsumeTouchEvent = true;
                        }
                        break;
                    case Gravity.TOP:
                        if ((Math.abs(event.getRawX() - mDownX) >= mMinDisallowDispatch) && b) {
                            // 当抽屉在上侧,手指在X轴移动的距离大于临界值,并且子视图消费了Move事件,则标记为子视图已经消费
                            isChildConsumeTouchEvent = true;
                        } else if (event.getRawY() - mDownY < -mMinDisallowDispatch) {
                            // 当Y轴方向移动的距离大于临界值的时候,标记为抽屉消费了事件,这时候需要移动抽屉
                            isConsumeTouchEvent = true;
                            goToConsumeTouchEvent = true;
                        }
                        break;
                }
                if (goToConsumeTouchEvent) {
                    // 如果自己消费了事件,则下发TOUCH_CANCEL事件(防止Button一直处于被按住的状态)
                    MotionEvent obtain = MotionEvent.obtain(event);
                    obtain.setAction(MotionEvent.ACTION_CANCEL);
                    super.dispatchTouchEvent(obtain);
                }
            }
            break;
        }

        if (isChildConsumeTouchEvent || !isConsumeTouchEvent) {
            // 自己未消费之前,先下发给子View
            super.dispatchTouchEvent(event);
        } else if (isConsumeTouchEvent && !isChildConsumeTouchEvent) {
            // 如果自己消费了,则不给子View
            performDispatchTouchEvent(event);
        }

        switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            if (!isConsumeTouchEvent && !isChildConsumeTouchEvent) {
                // 如果子View以及自己都没消化,则自己消化,防止点击一下,抽屉卡住
                performDispatchTouchEvent(event);
            }
            isConsumeTouchEvent = false;
            isChildConsumeTouchEvent = false;
            isTouchDown = false;
            break;
        }

        return true;
    }

}

写的有点复杂,对事件分发理解的可能该不够,哈哈。

下面是判断一次Touch事件是否有落在响应区域内。

/** 是否点击在响应区域 */
private boolean isDownInRespondArea(MotionEvent event) {
    float curTranslation = getCurTranslation();
    float x = event.getRawX();
    float y = event.getRawY();
    switch (mTouchViewGravity) {
        case Gravity.LEFT:
            if (x > curTranslation - mOpenedTouchViewSize && x < curTranslation) {
                return true;
            }
            break;
        case Gravity.RIGHT:
            if (x > curTranslation && x < curTranslation + mOpenedTouchViewSize) {
                return true;
            }
            break;
        case Gravity.BOTTOM:
            if (y > curTranslation && y < curTranslation + mOpenedTouchViewSize) {
                return true;
            }
            break;
        case Gravity.TOP:
            if (y > curTranslation - mOpenedTouchViewSize && y < curTranslation) {
                return true;
            }
            break;
        default:
            break;
    }
    return false;
}

上面说到两个响应区,一个是打开时的,一个是关闭时的,响应区的打开,也是提供给外部设置的:

/** 设置关闭状态下,响应触摸事件的控件宽度 */
public void setTouchSizeOfClosed(int width) {
    if (width == 0 || width < 0) {
        mClosedTouchViewSize = dip2px(mContext, TOUCH_VIEW_SIZE_DIP);
    } else {
        mClosedTouchViewSize = width;
    }
    ViewGroup.LayoutParams lp = mTouchView.getLayoutParams();
    if (lp != null) {
        if (isHorizontalGravity()) {
            lp.width = mClosedTouchViewSize;
            lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
        } else {
            lp.height = mClosedTouchViewSize;
            lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
        }
        mTouchView.requestLayout();
    }
}

/** 设置打开状态下,响应触摸事件的控件宽度 */
public void setTouchSizeOfOpened(int width) {
    if (width <= 0) {
        mOpenedTouchViewSize = dip2px(mContext, TOUCH_VIEW_SIZE_DIP);
    } else {
        mOpenedTouchViewSize = width;
    }
}

抽屉在滑动的时候有很多事件,在各个事件触发的地方做个回调。
因为需要回调的事件比较多,所以使用内部类实现接口,这样设置回调接口的时候就不用去实现一些没必要的回调方法:

public void setDrawerCallback(DrawerCallback drawerCallback) {
    this.mDrawerCallback = drawerCallback;
}

public interface DrawerCallback {

    void onStartOpen();

    void onEndOpen();

    void onStartClose();

    void onEndClose();

    void onPreOpen();

    /**
     * 正在移动回调
     * @param gravity
     * @param translation 移动的距离(当前移动位置到边界的距离,永远为正数)
     */
    void onTranslating(int gravity, float translation);
}

public static class DrawerCallbackAdapter implements DrawerCallback {

    @Override
    public void onStartOpen() {

    }

    @Override
    public void onEndOpen() {

    }

    @Override
    public void onStartClose() {

    }

    @Override
    public void onEndClose() {

    }

    @Override
    public void onPreOpen() {

    }

    @Override
    public void onTranslating(int gravity, float translation) {

    }
}

结束语:

后期有维护,有问题请留言,谢谢。

2015年12月12日17:29:41:

新增抽屉留白功能,支持抽屉拉出一部分,留出部分空白区域:详见: gitHub
抽屉留白

源码分享:

为了不断更新,我把源码提交到了gitHub
gitHub:https://github.com/a740169405/GenericDrawerLayout

CSDN下载的可能不是最新代码,建议到gitHub下载,当然前提是可以打开gitHub。
CSDN下载:http://download.csdn.net/detail/a740169405/9253119

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

Android 自定义万能的抽屉布局(侧滑菜单)GenericDrawerLayout 的相关文章

随机推荐

  • PCA(主成分分析)降维可视化Matlab实现

    实现一个动态展示二维到一维的实例 以及通过使用PCA对其进行简单降维 1 二维数据降维动态演示 下图通过使用投影关系将二维点集映射到一维直线上 直观上展示了二维到一维的降维和数据的映射关系 下图使用含有噪声圆的降维 可用于讲解kernel
  • 这二维码也太美了吧!利用AI绘画[Stable Diffusion的 ControlNet]生成爆火的艺术风格二维码

    文章目录 引子 爆火的艺术二维码 这种艺术二维码是如何制作出来的 ControlNet 介绍 ControlNet的限制条件 边缘检测示例 人体姿态检测示例 使用Canny边缘检测和Openpose有什么区别 安装稳定扩散控制网Contro
  • 服务器在使用过程中如何保护数据

    在租用服务器搭建网站运营的时候 除了保证网站的正常运营之外 对于网站数据安全的保护也不容忽视 那么租用服务器的 时候如何做好防御呢 租用服务器建站的时候如何做好数据库的安全工作呢 数据库的备份工作 对于数据库备份主要分为以下几种 完全备份
  • 【GAN 01】初识GAN

    本文是对http www seeprettyface com research notes html的学习笔记 评价指标 Inception Score评价图片质量 真实图片是233分 越高越好 FID反应生成图片的多样性 越低越好 一 初
  • 【第36篇】SwinIR(超分)

    文章目录 摘要 一 简介 二 相关工作 2 1 图像恢复 2 2 视觉转换器 三 方法 3 1 网络架构 3 2 剩余旋转变压器块 四 实验 4 1 实验设置 4 2 消融研究与讨论 4 3 图像 SR 的结果 4 4 JPEG 压缩伪影减
  • spring mvc:注解@ModelAttribute妙用

    在Spring mvc中 注解 ModelAttribute是一个非常常用的注解 其功能主要在两方面 运用在参数上 会将客户端传递过来的参数按名称注入到指定对象中 并且会将这个对象自动加入ModelMap中 便于View层使用 br gt
  • C语言 - AES软件加解密算法

    概述 AES RIJNDAEL算法是一个数据块长度盒密钥长度都可变的分组加密算法 其数据块长度和密钥长度都可独立地选定为大于等于128位且小于等于256位的32位任意倍数 深入学习请参考 密码学 书籍 谢谢各位参阅 验证环境 STM32F4
  • matlab怎么看输出电压纹波,Boost变换器的能量传输模式和输出纹波电压分析.pdf

    第26卷第5期 中国电机工程学报 01 26No 5Mar 2006 2006年3月 oftheCSEE 2006Chin Soc for Proceedings Elec Eng 文章编号 0258 8013 2006 05 0119 0
  • 在 Windows 下安装 COCO API(pycocotools)

    本内容将介绍在 Windows 下安装 COCO API pycocotools 本来 COCO 对 Windows 是不支持的 不过为了支持 Windows 有人对 COCO 做了一些修改 下面是 COCO 在 GitHub 上源码地址信
  • Echarts 大数据可视化实现

    全国空气质量AQI PM2 5数据可视化源码 1 编程环境为anaconda Jupyter 以下源码已划分好 如使用Jupyter环境编写 请按照顺序写在不同代码块中 注意 若将程序写在一个代码块中 将无法运行 2 源码代码注释的部分为单
  • 【代码随想录】字符串刷题

    字符串刷题 反转字符串 反转字符串II 替换空格 反转字符串中的单词 左旋转字符串 实现 strStr 重复的子字符串 关于字符串类的题目 要不要使用库函数呢 如果使用库函数可以直接做出来 建议不要使用库函数 如果库函数只是题目的一部分 可
  • 关于SYSTICK的COUNTFLAG标志的小疑惑

    前不久在研究SYSTICK有关问题阅读相关技术资料时 无意间产生了个小疑惑 问题是这样的 我们知道SYSTICK定时器是个24位向下计数器 每当发生从1记到0时会让一个名为COUNTFLAG的标志位置1 如果此时SYSTICK的滴答中断请求
  • ChatGPT集锦

    目录 1 一条指令让ChatGPT变的更强大 2 对ChatGPT提问时 常见的10种错误描述 3 Custom instructions如何设置 4 Custom Instructions如何打开 1 一条指令让ChatGPT变的更强大
  • JDBC批量操作

    使用PreparedStatement实现批量数据操作 通过前面学习我们知道 update delete本身就有批量操作的效果 但insert插入数据就需要用到批量操作 实现用PreparedStatement实现更高效的批量插入 用一道练
  • 生成GitHub项目目录的树形结构图

    之前在GitHub中看别人项目的README文档时 总是发现其中会有整个项目的目录结构图 当时还想着是什么工具或者命令快捷生成的 这几天由于整理大批量的文件夹 想看清文件夹下到底有什么文件或者目录 不想一个个的打开看 想直观的看到这个数据收
  • 深度学习系列26:transformer机制

    1 多头注意力机制 首先补充一下注意力和自注意力区别 自注意力有3个矩阵KQV 而注意力只有KV 可以理解为最终结果被用来当做Q了 多头注意力机制在自注意力机制上又加了一层多头的概念 即图中从多个不同角度做attention 用不同的方式初
  • 科学计数法e

    一 基本运算 个人理解 e就表示10 数字很大的数 一般我们用科学记数法表示 例如6230000000000 我们可以用6 23 10 12表示 而它含义是什么呢 从直面上看是将数字6 23中6后面的小数点向右移去12位 若将6 23 10
  • 2017 年 IT 界最严重的裁员事件汇总

    点击上方 程序员大咖 选择 置顶公众号 关键时刻 第一时间送达 裁员年年都有 今年特别多从微软 Oracle IBM 到思科 HPE 再到雅虎 stackoverflow 无论是处于转型变革中的老牌巨头 还是日渐成熟的创新型公司 在动荡的科
  • winform程序:newtonsoft json 序列化时出现 “unterminated string. Excepted delimiter..."

    在写一个winform程序时出现上述错误 开始以为newtonsoft 第三方插件对读取的字符串的长度有限制 也搜索没有用 后来通过读取文件 发现可以序列化 排除newtonsoft插件的问题 后来确定是winform的textbox控件的
  • Android 自定义万能的抽屉布局(侧滑菜单)GenericDrawerLayout

    转载请注明出处 http blog csdn net a740169405 article details 49720973 前言 大家应该对侧滑菜单很熟悉了 大多数是从左侧滑出 其实实现原理是v4支持包提供的一个类DrawerLayout