效果

注意 本节内容基于上一篇博客的代码继续编写code
https://blog.csdn.net/u011109881/article/details/112427623

本节知识点

1.如何创建View的截图
2.值动画练习
3.贝塞尔曲线的使用
4.画笔使用 onDraw练习
5.View.getRowX VS View.getX
6.获取状态栏高度
7.插值器使用
8.帧动画练习

步骤分析

1.原理
将原来的View隐藏,拖动的时候利用WindowManager创建一个新View(只是一个截图),拖动的其实只是截图,我们可以将截图放在Decor View的层级(这里是我的猜测,视频里面说的是放在WindowManager上 但我不理解WindowManager是哪一层),这样就能在状态栏上面拖动
2.按下的时候将原先的View隐藏
2.1创建一个截图
2.2将创建的截图保存到DragView并让其在onDraw绘制 注意保存截图的显示位置
2.3将创建的截图和DragView一起显示到界面(在onDraw绘制)
3.移动的时候 不停绘制贝塞尔曲线以及截图
3.1 遇到问题originView的位置不对,手指点的位置不对
mOriginView.getX(), mOriginView.getY()的方式不对 这个是相对于父布局的位置 我们需要相对屏幕的位置,另外需要确保固定圆在mOriginView的中心就要加上view宽高的一半,同时还要减去图状态栏高度
4.对手指抬起做监听
4.1如果 抬起时距离不大,view回弹
4.2计算回弹轨迹,添加回弹动画
4.3利用插值器,回弹到原始位置的时候添加抖动效果
4.4去除创建的截图并显示出原先的View
4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源
4.6通知调用者,View被删除成功

大部分代码

工具类

class Utils {//2.1 创建截图public static Bitmap getBitmapByView(View view) {view.buildDrawingCache();Bitmap bitmap = view.getDrawingCache();return bitmap;}public static float dp2px(float dp, Context context) {return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());}//获取状态栏高度public static float getStatusBarHeight(Context context) {//获取status_bar_height资源的IDint resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");if (resourceId > 0) {return context.getResources().getDimensionPixelSize(resourceId);}//没有获取到 取0return 0;}//按照百分比在由point1 point2组成的线段上取点public static PointF getPointByPercent(PointF point1, PointF point2, float percent) {return new PointF(evaluateValue(percent, point1.x, point2.x), evaluateValue(percent, point1.y, point2.y));}//Number是int float等基本数字类型的父类public static float evaluateValue(float percent, Number start, Number end) {return start.floatValue() + (end.floatValue() - start.floatValue())* percent;}
}

Activity

public class MainActivity extends AppCompatActivity {private final String TAG = "MainActivity";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textView);DragBoomView.attachToView(textView, view -> {//4.6通知到调用者,View被删除成功Toast.makeText(MainActivity.this, "原先的View1被删除", Toast.LENGTH_SHORT).show();view.setVisibility(View.GONE);});TextView textView2 = findViewById(R.id.textView2);DragBoomView.attachToView(textView2, view -> {Toast.makeText(MainActivity.this, "原先的View2被删除", Toast.LENGTH_SHORT).show();view.setVisibility(View.GONE);});TextView textView3 = findViewById(R.id.textView3);DragBoomView.attachToView(textView3, view -> {Toast.makeText(MainActivity.this, "原先的View3被删除", Toast.LENGTH_SHORT).show();view.setVisibility(View.GONE);});}
}

自定义View(包含截图+贝塞尔曲线)

class DragBoomView extends View {private static final String TAG = "DragBoomView";private PointF mFixPoint;private PointF mFingerPoint;private Paint mPaint;private float mFixPointInitRadius = 10;//固定圆初始半径private float mMinFixPointRadius = 5;//固定圆最小半径 如果比这个更小 不进行绘制private float mFixPointChangedRadius;//挪动手指后的固定圆半径private float mFingerPointRadius = 10;private DragBoomViewTouchListener mDragBoomViewTouchListener;private Bitmap mCaptureView;//截图public DragBoomView(Context context) {this(context, null);}public DragBoomView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public DragBoomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);initPaint();mFixPointInitRadius = Utils.dp2px(mFixPointInitRadius, context);mFingerPointRadius = Utils.dp2px(mFingerPointRadius, context);mMinFixPointRadius = Utils.dp2px(mMinFixPointRadius, context);}public static void attachToView(View textView, DragBoomViewTouchListener.DragViewDisappearListener disappearListener) {textView.setOnTouchListener(new DragBoomViewTouchListener(textView, disappearListener));}private void initPaint() {mPaint = new Paint();mPaint.setDither(true);mPaint.setAntiAlias(true);mPaint.setColor(Color.RED);}@Overrideprotected void onDraw(Canvas canvas) {if (mFingerPoint == null || mFixPoint == null) {return;}//绘制固定圆if (isNeedShowBezier()) {//当手指离固定圆太远 不绘制固定圆canvas.drawCircle(mFixPoint.x, mFixPoint.y, mFixPointChangedRadius, mPaint);canvas.drawPath(getBezierPath(), mPaint);}//绘制跟随手指的圆canvas.drawCircle(mFingerPoint.x, mFingerPoint.y, mFingerPointRadius, mPaint);//2.3将创建的截图和DragView一起显示到界面(在onDraw绘制)if (mCaptureView != null) {canvas.drawBitmap(mCaptureView, mFingerPoint.x - mCaptureView.getWidth() / 2, mFingerPoint.y - mCaptureView.getHeight() / 2, mPaint);}}//计算两点之间的距离double distanceOfPoints(PointF point1, PointF point2) {//勾股定理return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));}//    @Override
//    public boolean onTouchEvent(MotionEvent event) {
//        switch (event.getAction()) {
//            case MotionEvent.ACTION_DOWN:
//                mFixPoint = new PointF();
//                mFingerPoint = new PointF();
//                mFixPoint.x = event.getX();
//                mFixPoint.y = event.getY();
//                mFingerPoint.x = event.getX();
//                mFingerPoint.y = event.getY();
//            case MotionEvent.ACTION_MOVE:
//                mFingerPoint.x = event.getX();
//                mFingerPoint.y = event.getY();
//                break;
//            case MotionEvent.ACTION_UP:
//                break;
//        }
//        //根据手指的移动 计算两点之间的距离 根据距离改变mFixPoint的半径
//        mFixPointChangedRadius = calculateFixPointRadius(distanceOfPoints(mFingerPoint, mFixPoint));
//        invalidate();
//        return true;
//    }private float calculateFixPointRadius(double distanceOfPoints) {return mFixPointInitRadius - (float) distanceOfPoints / 20f;//除数(20f)越大,代表半径随距离变化的程度越小}private Path getBezierPath() {Path bezierPath = new Path();//计算四点坐标//先计算∠a ∠a = arctan((r1.y-r2.y)/(r1.x-r2.x))double angleA = Math.atan((mFixPoint.y - mFingerPoint.y) / (mFixPoint.x - mFingerPoint.x));//计算x y以及 x' y'float x = (float) (Math.sin(angleA) * mFixPointChangedRadius);float y = (float) (Math.cos(angleA) * mFixPointChangedRadius);float x2 = (float) (Math.sin(angleA) * mFingerPointRadius);float y2 = (float) (Math.cos(angleA) * mFingerPointRadius);//四个点坐标分别为 A1  B1 A2 B2 他们坐标表示为float A1x = mFixPoint.x + x;float A1y = mFixPoint.y - y;float B1x = mFixPoint.x - x;float B1y = mFixPoint.y + y;float A2x = mFingerPoint.x + x2;float A2y = mFingerPoint.y - y2;float B2x = mFingerPoint.x - x2;float B2y = mFingerPoint.y + y2;float controlPint1x = (mFixPoint.x + mFingerPoint.x) * 0.5f;float controlPint1y = (mFixPoint.y + mFingerPoint.y) * 0.5f;//绘制路径为A1 A2 B2 B1bezierPath.moveTo(A1x, A1y);bezierPath.quadTo(controlPint1x, controlPint1y, A2x, A2y);bezierPath.lineTo(B2x, B2y);bezierPath.quadTo(controlPint1x, controlPint1y, B1x, B1y);bezierPath.close();return bezierPath;}public void setDragViewTouchListener(DragBoomViewTouchListener dragBoomViewTouchListener) {mDragBoomViewTouchListener = dragBoomViewTouchListener;}public void setCaptureView(Bitmap bitmap) {this.mCaptureView = bitmap;}public void updatePosition(float rawX, float rawY) {if (mFingerPoint == null) {mFingerPoint = new PointF();}mFingerPoint.x = rawX;mFingerPoint.y = rawY;mFixPointChangedRadius = calculateFixPointRadius(distanceOfPoints(mFingerPoint, mFixPoint));invalidate();}//2.2将创建的截图保存到DragView并让其在onDraw绘制 注意保存截图的显示位置public void initPoints(float pointX, float pointY) {mFixPoint = new PointF(pointX, pointY);mFingerPoint = new PointF(pointX, pointY);invalidate();}private boolean isNeedShowBezier() {return mFixPointChangedRadius > mMinFixPointRadius;}public void dealWithActionUp() {if (this.isNeedShowBezier()) { //4.1如果 抬起时距离不大,view回弹playBackAnimate();} else {//4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源if (mDragBoomViewTouchListener != null) {//4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源mDragBoomViewTouchListener.dismiss(mFingerPoint);}}}private void playBackAnimate() {//4.2计算回弹轨迹,添加回弹动画//ValueAnimator 值变化的动画  getAnimatedValue由0变化到1ValueAnimator animator = ObjectAnimator.ofFloat(1);animator.setDuration(250);final PointF start = new PointF(mFingerPoint.x, mFingerPoint.y);final PointF end = new PointF(mFixPoint.x, mFixPoint.y);animator.addUpdateListener(animation -> {float percent = (float) animation.getAnimatedValue();// 0 - 1PointF pointF = Utils.getPointByPercent(start, end, percent);// 用代码更新拖拽点updatePosition(pointF.x, pointF.y);});// 4.3利用插值器,回弹到原始位置的时候添加抖动效果// 设置一个差值器 在结束的时候有一个弹动效果animator.setInterpolator(new OvershootInterpolator(3f));//3f表示晃动强度较大 数值越大效果越强animator.start();animator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mDragBoomViewTouchListener != null) {//4.4去除创建的截图并显示出原先的View// 还要通知 TouchListener 移除当前View 然后显示静态的 ViewmDragBoomViewTouchListener.reset();}}});}
}

Listener

class DragBoomViewTouchListener implements View.OnTouchListener {private DragBoomView mDragView;private WindowManager mWindowManager;private Context mContext;private View mOriginView;private WindowManager.LayoutParams mParams;// 爆炸帧动画private FrameLayout mBombFrame;private ImageView mBombImage;private DragViewDisappearListener mDisappearListener;public DragBoomViewTouchListener(View mOriginView, DragViewDisappearListener disappearListener) {this.mOriginView = mOriginView;this.mContext = mOriginView.getContext();mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);mDragView = new DragBoomView(mContext);mDragView.setDragViewTouchListener(this);mParams = new WindowManager.LayoutParams();// 背景要透明mParams.format = PixelFormat.TRANSPARENT;//爆炸动画初始化mBombFrame = new FrameLayout(mContext);mBombImage = new ImageView(mContext);mBombImage.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));mBombFrame.addView(mBombImage);this.mDisappearListener = disappearListener;}@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:int[] location = new int[2];mOriginView.getLocationOnScreen(location);//3.1 遇到问题originView的位置不对,手指点的位置不对//location是view左上角相对屏幕的位置,需要确保固定圆在mOriginView的中心就要加上view宽高的一半,同时还要减去图状态栏高度mDragView.initPoints(location[0] + (mOriginView.getMeasuredWidth() / 2), location[1] + (mOriginView.getMeasuredHeight() / 2) - Utils.getStatusBarHeight(mContext));Bitmap bitmap = Utils.getBitmapByView(mOriginView);mDragView.setCaptureView(bitmap);mWindowManager.addView(mDragView, mParams);//2.按下的时候将原先的View隐藏mOriginView.setVisibility(View.INVISIBLE);case MotionEvent.ACTION_MOVE://3.移动的时候 不停绘制贝塞尔曲线以及截图//这里传的点和down的点不统一 我认为不太好mDragView.updatePosition(event.getRawX(), event.getRawY() - Utils.getStatusBarHeight(mContext));break;case MotionEvent.ACTION_UP://4.对手指抬起做监听mDragView.dealWithActionUp();break;}return true;}public void reset() {// 把创建的贝塞尔曲线以及截图删除mWindowManager.removeView(mDragView);// 把原来的View显示mOriginView.setVisibility(View.VISIBLE);}//帧动画public void dismiss(PointF pointF) {// 移除截图的ViewmWindowManager.removeView(mDragView);// 要在 mWindowManager 添加一个爆炸动画mWindowManager.addView(mBombFrame, mParams);mBombImage.setBackgroundResource(R.drawable.anim_bubble_pop);AnimationDrawable drawable = (AnimationDrawable) mBombImage.getBackground();mBombImage.setX(pointF.x - drawable.getIntrinsicWidth() / 2);mBombImage.setY(pointF.y - drawable.getIntrinsicHeight() / 2);drawable.start();// 等它执行完之后我要移除掉这个 爆炸动画也就是 mBombFramemBombImage.postDelayed(new Runnable() {@Overridepublic void run() {mWindowManager.removeView(mBombFrame);// 通知一下外面该消失if (mDisappearListener != null) {mDisappearListener.viewDismiss(mOriginView);}}}, getAnimationDrawableTime(drawable));}private long getAnimationDrawableTime(AnimationDrawable drawable) {int numberOfFrames = drawable.getNumberOfFrames();long time = 0;for (int i = 0; i < numberOfFrames; i++) {time += drawable.getDuration(i);}return time;}public interface DragViewDisappearListener {void viewDismiss(View view);}
}

帧动画

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"android:oneshot="true"><itemandroid:drawable="@drawable/pop1"android:duration="100" /><itemandroid:drawable="@drawable/pop2"android:duration="100" /><itemandroid:drawable="@drawable/pop3"android:duration="100" /><itemandroid:drawable="@drawable/pop4"android:duration="100" /><itemandroid:drawable="@drawable/pop5"android:duration="100" /></animation-list>

后记

1.原视频中将原先的view传递到自定义View并给他注册TouchListener,事实上这样有点问题,因为这样该View不再能注册其他TouchListener,如果在其他地方注册,就会导致拖动消失的功能消失。不过因为既然想要拖动该View,应该也不需要在其他地方注册TouchListener了吧?
2.在拖动的时候发现状态栏变成黑色,如下图。事实上,我不清楚创建的截图放到了什么位置,因为我使用Layout Inspector的时候没有看到创建的截图View在哪里

但是我想我们应该可以取得DecorView 因为DecorView本身是一个FrameLayout,我们可以将截图放在DecorView的第一个的位置(index从0开始计算),我们可以取得Decor View的引用 保存原来的View的引用,先删除掉原先的所有子view,然后重新按照顺序添加。当然,这只是一个思路,没有验证。


3.DragBoomViewTouchListener 和DragBoomView 职责不清,DragBoomViewTouchListener 明明是个listener 但是里面包含太多的功能,MainActivity普通的View通过DragBoomView与DragBoomViewTouchListener 建立联系,感觉耦合性较高,可以把更多的功能放到DragBoomView 中。
4.回弹动画应该可以使用translationX和translationY动画结合代替

    private void playBackAnimate() {ObjectAnimator translationX = ObjectAnimator.ofFloat(this, "translationX", mFingerPoint.x - mCaptureView.getWidth() / 2, mFixPoint.x - mCaptureView.getWidth() / 2);ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", mFingerPoint.y - mCaptureView.getHeight() / 2, mFixPoint.y - mCaptureView.getHeight() / 2);AnimatorSet animatorSet = new AnimatorSet();animatorSet.playTogether(translationX, translationY);animatorSet.setDuration(1000 * 2);animatorSet.start();animatorSet.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mDragBoomViewTouchListener != null) {mDragBoomViewTouchListener.reset();}}});}

但是实际跑的时候发现点似乎不对,具体原因还没有找到,也不想纠结太多。但是我认为可能的原因是,贝塞尔曲线绘制的原始点和拖动点都位于View的中心,而位移动画计算的时候我们使用View的左上角进行计算,因此可能存在偏差。在本篇中,最容易出问题的是各种坐标。事实上,对于本节位置相关内容,我们需要考虑以下4点
a 是否需要加上状态栏高度
b 绘制点或者图形的时候是否需要考虑View的宽高
c 进行位移动画时应该以图像的左上角为中心进行计算。
事实上本节中

mDragView.initPoints(location[0] + (mOriginView.getMeasuredWidth() / 2), location[1] + (mOriginView.getMeasuredHeight() / 2) - Utils.getStatusBarHeight(mContext));
mDragView.updatePosition(event.getRawX(), event.getRawY() - Utils.getStatusBarHeight(mContext));

两种不同的点的计算方式很容易让人产生误会。我觉得比较好的方式是记录下手指点击的坐标与View的中心相差的差值,统一在dragView中进行处理,而不是通过外部DragBoomViewTouchListener的不同标准传到DragBoomView再对坐标进行二次纠正。
5.小bug:

如图 拖动的点强制表示为View的正中心,使用第4条中的方式有可能规避这个问题。
6.在下一节视频中,使用到了TypeEvaluator,我认为在本节中的回弹动画也可以使用TypeEvaluator代替,如下所示 而不是写两个函数getPointByPercent和evaluateValue,这样写不够通用

    private void playBackAnimate() {//4.2计算回弹轨迹,添加回弹动画final PointF start = new PointF(mFingerPoint.x, mFingerPoint.y);final PointF end = new PointF(mFixPoint.x, mFixPoint.y);BezierTypeEvaluator evaluator = new BezierTypeEvaluator();ValueAnimator animator = ObjectAnimator.ofObject(evaluator,start,end);animator.setDuration(250);animator.addUpdateListener(animation -> {//            float percent = (float) animation.getAnimatedValue();// 0 - 1
//            PointF pointF = Utils.getPointByPercent(start, end, percent);PointF pointF = (PointF) animation.getAnimatedValue();// 用代码更新拖拽点updatePosition(pointF.x, pointF.y);});//省略其他代码}static class BezierTypeEvaluator implements TypeEvaluator<PointF>{@Overridepublic PointF evaluate(float fraction, PointF startValue, PointF endValue) {float pointX = startValue.x + (endValue.x - startValue.x) * fraction;float pointY = startValue.y + (endValue.y - startValue.y) * fraction;return new PointF(pointX, pointY);}}

code:
https://github.com/caihuijian/learn_darren_android.git

红橙Darren视频笔记 任意控件实现拖动消失爆炸效果相关推荐

  1. 红橙Darren视频笔记 一个控件显示两种颜色的文字 画笔的使用

    需求分析 1.一行文字显示两种颜色 2.颜色变化可以从右到左或者从左到右 3.能够随着view pager切换 思路: a.继承View:需要重写onMeasure onDraw方法 b.继承Text ...

  2. 红橙Darren视频笔记 贝塞尔曲线实现消息拖拽粘性效果 画笔练习

    效果图 要实现这样的效果 我们要先知道那段弧线是如何绘制的,实际上那段弧线就是贝塞尔曲线 一. 什么是贝塞尔曲线 https://www.jianshu.com/p/8f82db9556d2 上面有个 ...

  3. 红橙Darren视频笔记 ViewGroup事件分发分析 基于API27

    本节目标,通过案例,先看程序运行结果,然后跟踪源码,理解为什么会有这样的输出,继而理解view group的分发机制,感觉和证明题很像呢. 考虑以下程序的运行结果: case1: public cla ...

  4. 红橙Darren视频笔记 UML图简介

    整体架构复制自红橙原视频的课堂笔记 因为他这一课没有博客,所以没有转载链接,CSDN没有转载地址是无法作为转载类型的文章发表的,暂时标记为原创 参考链接 https://blog.csdn.net/r ...

  5. 红橙Darren视频笔记 代理模式 动态代理和静态代理

    红橙Darren视频笔记 代理模式 动态代理和静态代理(Android API 25) 关于代理模式我之前有过相关的介绍: https://blog.csdn.net/u011109881/artic ...

  6. 红橙Darren视频笔记 类加载机制(API28) 自己写个热修复 查看源码网站

    第一部分 类加载机制 一个Activity是如何被Android虚拟机找到的? 在之前的文章 红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109 ...

  7. 红橙Darren视频笔记 利用阿里巴巴AndFix进行热修复

    注意 由于AndFix在2017年左右就停止更新了,在最新版本的apk上遇到很多问题,我最终也没有成功进行热修复.本节主要是学习热修复的原理 在上一篇 红橙Darren视频笔记 自己捕获异常并保存到本 ...

  8. 红橙Darren视频笔记 Behavior的工作原理源码分析

    主要coordinatorlayout的代码来自coordinatorlayout-1.0.0-sources.jar 本文从源码介绍 CoordinatorLayout 的 behavior 怎么工 ...

  9. 红橙Darren视频笔记 仿QQ侧滑效果

    这一篇没有什么新的内容 就是改写 红橙Darren视频笔记 仿酷狗侧滑效果 的侧滑的效果 1.去掉淡入淡出效果 2.加上黑色模板效果 效果: 去掉淡入淡出效果很简单 就是注释掉onScrollChan ...

最新文章

  1. 数据库开发个人总结(ADO.NET小结)
  2. 非程序员如何使用 Git——版本控制你的生活
  3. Centos 7 64位 minimal 最小化安装的系统中静默安装oracle 11g r2(无图形化安装)
  4. flask-bootstrap-高亮-下划线-删除线-加粗-斜体
  5. Leetcode题库 125.验证回文串(双指针 C实现)
  6. sublime text3c语言编译运行,c – Sublime text 3 – 编译程序并在终端中运行
  7. 作者:陈兴鹏(1963-),男,兰州大学资源环境学院教授、博士生导师。
  8. 【C语言笔记进阶篇】第二章:字符串函数和内存函数
  9. netty心跳过程中 发送消息失败_netty心跳机制和断线重连(四)
  10. 车速与档位匹配关系_档位与速度匹配法则 每个档位的速度范围
  11. springboot application.properties server.port配置小问题
  12. 【clickhouse】ClickHouse官方中文文档 阅读笔记
  13. kubenetes 1.4安装kube-UI
  14. Javascript第四章函数function也是数据类型第六课
  15. presto 使用 部署_探秘Presto+Alluxio高效云端SQL查询
  16. iperf命令linux,Linux iperf 用法介绍
  17. 推荐10款社群运营必备工具
  18. 随笔记录——numpy4(伪随机数生成)
  19. ROS从入门到精通0-2:Win10+Ubuntu双系统安装、配置、卸载保姆级图文教程
  20. grunt教程--初涉grunt

热门文章

  1. mybatis-plus排除非表中字段
  2. 关于MySQL buffer pool的预读机制
  3. FreeRTOS 计数信号量
  4. Java基础学习 -- I/O系统、流
  5. 管理和维护RHCS集群
  6. mysql explain insert_mysql explain详解
  7. python django mysql结果获取_Django中从mysql数据库中获取数据传到echarts方式
  8. 从问题出发,解密Oracle rdba结构
  9. 嘉年华回顾丨李海翔带你解密腾讯TDSQL数据库的技术与未来
  10. 实战演练:MySQL RPM包定制化制作全过程