转载请注明出处:
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());// 初始化用来相应触摸的透明ViewmTouchView = 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();}// 隐藏TouchViewsetVisibility(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() {@Overridepublic void onAnimationStart(Animator animation) {// 回掉事件if (!AnimStatus.OPENING.equals(mAnimStatus) && !AnimStatus.OPENED.equals(mAnimStatus)) {if (mDrawerCallback != null) {mDrawerCallback.onStartOpen();}}// 更新状态mAnimStatus = AnimStatus.OPENING;}@Overridepublic 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() {@Overridepublic void onAnimationStart(Animator animation) {if (!AnimStatus.CLOSING.equals(mAnimStatus) && !AnimStatus.CLOSED.equals(mAnimStatus)) {if (mDrawerCallback != null) {mDrawerCallback.onStartClose();}}mAnimStatus = AnimStatus.CLOSING;}@Overridepublic 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);}@Overridepublic void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {super.requestDisallowInterceptTouchEvent(disallowIntercept);}@Overridepublic 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) {// 自己未消费之前,先下发给子Viewsuper.dispatchTouchEvent(event);} else if (isConsumeTouchEvent && !isChildConsumeTouchEvent) {// 如果自己消费了,则不给子ViewperformDispatchTouchEvent(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 {@Overridepublic void onStartOpen() {}@Overridepublic void onEndOpen() {}@Overridepublic void onStartClose() {}@Overridepublic void onEndClose() {}@Overridepublic void onPreOpen() {}@Overridepublic 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

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

  1. Android自定义View之仿QQ侧滑菜单实现

    最近,由于正在做的一个应用中要用到侧滑菜单,所以通过查资料看视频,学习了一下自定义View,实现一个类似于QQ的侧滑菜单,顺便还将其封装为自定义组件,可以实现类似QQ的侧滑菜单和抽屉式侧滑菜单两种菜单 ...

  2. android中仿qq最新版抽屉,Android 自定义View实现抽屉效果

    Android 自定义View实现抽屉效果 说明 这个自定义View,没有处理好多点触摸问题 View跟着手指移动,没有采用传统的scrollBy方法,而是通过不停地重新布局子View的方式,来使得子 ...

  3. android自定义radiogroup,Android 自定义View实现任意布局的RadioGroup效果

    前言 RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局.所以在某些情况,比如多行多列布局,RadioGroup就并不适用 . 本篇文章通过继承RelativeLayout ...

  4. Android自定义TabActivity(实现仿新浪微博底部菜单更新UI)

    如今Android上很多应用都采用底部菜单控制更新的UI这种框架,例如新浪微博 点击底部菜单的选项可以更新界面.底部菜单可以使用TabHost来实现,不过用过TabHost的人都知道自定义TabHos ...

  5. Android开发_DrawerLayout实现抽屉布局

    布局文件代码 <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk ...

  6. Android自定义ListView实现仿微信侧滑删除

    经常在遇到问题第一时间都会在网上搜索解决的方法,因此看到很多前辈们的比较精辟的技术文章,学习了很多东西,现在将自己平时工作中开发的一些小功能坐下总结,也写出来,既方便自己理清思路记忆功能块实现思路,又 ...

  7. android自定义阴影的卡片布局,CardView卡片布局的简单使用

    有时候我们见到过App上有那种和卡片一样的布局,自定义的话太麻烦,于是有了大佬给我们集成了第三方,CardView卡片式布局设计 CardView简介 CardView继承自FrameLayout类. ...

  8. Android UI进阶之旅3 Material Design之侧滑菜单的两种实现

    ###侧滑菜单的两种实现 使用DrawerLayout,灵活度比较高. 使用DrawerLayout+NavigationView,这是谷歌对Material Design的一种标准化. ###使用D ...

  9. Android自定义view之基础知识

    Android自定义view之基础知识 虽然Android已经自带了很多实用的view和layout,加以调教能实现很美观的界面,但是有一些情况下,需要实现特殊的界面效果,比如我们比较熟悉的各种播放器 ...

  10. Android 自定义底部上拉控件的实现

    前言 又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手. 写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上 ...

最新文章

  1. 未来的程序员该如何选公司和谋规划?
  2. ubuntu中PyCharm的安装与卸载
  3. VTK:Utilities之BoundingBox
  4. 腾讯邓君:《王者荣耀》翻过的同步技术相关的三座大山
  5. 不常用≠没用 Win7容易忽略的四个功能
  6. js 验证各种格式类型的正则表达式
  7. java的对象对象映射_Java对象到对象映射器
  8. wp自定义帖子没标签_ofollow标签的作用有重大变化
  9. Jeecg-Boot导入附件异常解决
  10. Firefox内存占用过高解决方法
  11. 机器学习-监督学习之分类算法:K近邻法 (K-Nearest Neighbor,KNN)
  12. mysql日期相关的函数
  13. 【RPA面试题】Q2. If Activity和Flow Decision的区别是什么?
  14. CSS第四篇(复合选择器)
  15. Codeforces Round #439 (Div. 2) E. The Untended Antiquity(二维BIT)
  16. 制作 macOS High Sierra U盘
  17. 爬取贴吧上的图片到本地
  18. html创建关联程序,如何在控制面板中创建电子邮件默认程序关联
  19. Blend for Visual Studio 让XAML也可以像WinForm一样可视化设计,Blend 与Studio的区别
  20. 真香!取得软考证书可参加通信行业高级职称转评转升

热门文章

  1. 【POJ 3977】【折半枚举】【超大背包】Subset【暑期 No.7】
  2. 刷题记录 kuangbin带你飞专题一:简单搜索
  3. Redis在Window的启动方式(包括安装包也送)
  4. java 协变 逆变_JAVA中的协变与逆变
  5. html帮助文档看不了,Service Log按照文档设置之后,在web页面看不到,帮助文档的图片有点问题(看不到了),能不能处理一下...
  6. Linux源码包和脚本安装包的安装方法
  7. web开发:css基础
  8. python学习笔记:操作Excle
  9. 58-最小乘积(基本型)
  10. elementui中el-upload自定义上传方法中遇到的问题