Android 自定义万能的抽屉布局(侧滑菜单)GenericDrawerLayout
转载请注明出处:
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相关推荐
- Android自定义View之仿QQ侧滑菜单实现
最近,由于正在做的一个应用中要用到侧滑菜单,所以通过查资料看视频,学习了一下自定义View,实现一个类似于QQ的侧滑菜单,顺便还将其封装为自定义组件,可以实现类似QQ的侧滑菜单和抽屉式侧滑菜单两种菜单 ...
- android中仿qq最新版抽屉,Android 自定义View实现抽屉效果
Android 自定义View实现抽屉效果 说明 这个自定义View,没有处理好多点触摸问题 View跟着手指移动,没有采用传统的scrollBy方法,而是通过不停地重新布局子View的方式,来使得子 ...
- android自定义radiogroup,Android 自定义View实现任意布局的RadioGroup效果
前言 RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局.所以在某些情况,比如多行多列布局,RadioGroup就并不适用 . 本篇文章通过继承RelativeLayout ...
- Android自定义TabActivity(实现仿新浪微博底部菜单更新UI)
如今Android上很多应用都采用底部菜单控制更新的UI这种框架,例如新浪微博 点击底部菜单的选项可以更新界面.底部菜单可以使用TabHost来实现,不过用过TabHost的人都知道自定义TabHos ...
- Android开发_DrawerLayout实现抽屉布局
布局文件代码 <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk ...
- Android自定义ListView实现仿微信侧滑删除
经常在遇到问题第一时间都会在网上搜索解决的方法,因此看到很多前辈们的比较精辟的技术文章,学习了很多东西,现在将自己平时工作中开发的一些小功能坐下总结,也写出来,既方便自己理清思路记忆功能块实现思路,又 ...
- android自定义阴影的卡片布局,CardView卡片布局的简单使用
有时候我们见到过App上有那种和卡片一样的布局,自定义的话太麻烦,于是有了大佬给我们集成了第三方,CardView卡片式布局设计 CardView简介 CardView继承自FrameLayout类. ...
- Android UI进阶之旅3 Material Design之侧滑菜单的两种实现
###侧滑菜单的两种实现 使用DrawerLayout,灵活度比较高. 使用DrawerLayout+NavigationView,这是谷歌对Material Design的一种标准化. ###使用D ...
- Android自定义view之基础知识
Android自定义view之基础知识 虽然Android已经自带了很多实用的view和layout,加以调教能实现很美观的界面,但是有一些情况下,需要实现特殊的界面效果,比如我们比较熟悉的各种播放器 ...
- Android 自定义底部上拉控件的实现
前言 又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手. 写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上 ...
最新文章
- 未来的程序员该如何选公司和谋规划?
- ubuntu中PyCharm的安装与卸载
- VTK:Utilities之BoundingBox
- 腾讯邓君:《王者荣耀》翻过的同步技术相关的三座大山
- 不常用≠没用 Win7容易忽略的四个功能
- js 验证各种格式类型的正则表达式
- java的对象对象映射_Java对象到对象映射器
- wp自定义帖子没标签_ofollow标签的作用有重大变化
- Jeecg-Boot导入附件异常解决
- Firefox内存占用过高解决方法
- 机器学习-监督学习之分类算法:K近邻法 (K-Nearest Neighbor,KNN)
- mysql日期相关的函数
- 【RPA面试题】Q2. If Activity和Flow Decision的区别是什么?
- CSS第四篇(复合选择器)
- Codeforces Round #439 (Div. 2) E. The Untended Antiquity(二维BIT)
- 制作 macOS High Sierra U盘
- 爬取贴吧上的图片到本地
- html创建关联程序,如何在控制面板中创建电子邮件默认程序关联
- Blend for Visual Studio 让XAML也可以像WinForm一样可视化设计,Blend 与Studio的区别
- 真香!取得软考证书可参加通信行业高级职称转评转升
热门文章
- 【POJ 3977】【折半枚举】【超大背包】Subset【暑期 No.7】
- 刷题记录 kuangbin带你飞专题一:简单搜索
- Redis在Window的启动方式(包括安装包也送)
- java 协变 逆变_JAVA中的协变与逆变
- html帮助文档看不了,Service Log按照文档设置之后,在web页面看不到,帮助文档的图片有点问题(看不到了),能不能处理一下...
- Linux源码包和脚本安装包的安装方法
- web开发:css基础
- python学习笔记:操作Excle
- 58-最小乘积(基本型)
- elementui中el-upload自定义上传方法中遇到的问题