Android开发艺术探索-第三章-View的事件体系

御弟哥哥
• 阅读 1324

3.1 View的基础知识

  • 位置参数

    top、left、right、bottom,在3.0之后增加了x、y、translationX、translationY.这里的所有参数都是相对其父布局来说的.
    下面是具体的含义表示

    Android开发艺术探索-第三章-View的事件体系

    View参数

    其中参数的关系为

     x = left + translationX
           y = top + translationY 
  • MontionEvent和TouchSlop

    MontionEvent代表着触摸事件封装的数据,包括常用的Action和位置参数等.如上面图示,注意函数getRaw*()是相对与屏幕的.
    TouchSlop表示滑动的最小常量.是常量(int),不是具体的类.获取方式为:

     ViewConfiguration.get(getContext()).getScaledTouchSlop() 
  • VelocityTracker,GestureDetector和Scroller

    VelocityTracker用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。
    速度计算公式:

     速度 = (终点位置 - 起点位置) / 时间段 

    速度可能为负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,单位时间不一定是1秒钟,可以使用方法
    computeCurrentVelocity(xxx)指定单位时间是多少,单位是ms。例如通过computeCurrentVelocity(1000)来获取速度,手指在1s中
    滑动了100个像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)来获取速度,在100ms内手指只是滑动了
    10个像素,那么速度是10,即10(像素/100ms)。
    VelocityTracker的使用方式:

     //初始化
           VelocityTracker mVelocityTracker = VelocityTracker.obtain();
           //在onTouchEvent方法中
           mVelocityTracker.addMovement(event);
           //获取速度
           mVelocityTracker.computeCurrentVelocity(1000);
           float xVelocity = mVelocityTracker.getXVelocity();
           //重置和回收
           mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
           mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用 

    GestureDetector用于辅助检测用户的单击、滑动、长按、双击等行为。GestureDetector的使用比较简单,主要也是辅助检测常见的触屏事件。
    作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。

    Android开发艺术探索-第三章-View的事件体系

    GestureDetector

    Android开发艺术探索-第三章-View的事件体系

    DoubleTabListener

    Android开发艺术探索-第三章-View的事件体系

    GestureListener

     //自定义的View,实现相关接口(onGestureListener,onDoubleTabListener)
           GestureDetector mGestureDetector = 
                   new GestureDetector(this/*context*/,listener/*onGestureListener*/);
    
           //function onTouchEvent(...)或onTouchListener的onTouch(...)中,直接返回
           return mGestureDetector.onTouchEvent(event) 

    更多使用参见[参考2]

3.2 View的滑动

  • layout

     public void layout (int l, int t, int r, int b) 

    参数都是相对与父布局.

     @Override
           public boolean onTouchEvent(MotionEvent event) {
               int rawX = (int) (event.getRawX()); //相对与屏幕的坐标
               int rawY = (int) (event.getRawY());
               switch (event.getAction()) {
                   case MotionEvent.ACTION_DOWN:
                       // 记录触摸点坐标
                       lastX = rawX;
                       lastY = rawY;
                       break;
                   case MotionEvent.ACTION_MOVE:
                       // 计算偏移量
                       int offsetX = rawX - lastX;
                       int offsetY = rawY - lastY;
                       // 在当前left、top、right、bottom的基础上加上偏移量
                       layout(getLeft() + offsetX,
                               getTop() + offsetY,
                               getRight() + offsetX,
                               getBottom() + offsetY);
                       // 重新设置初始坐标
                       lastX = rawX;
                       lastY = rawY;
                       break;
               }
               return true;
           } 
  • offsetLeftAndRight和offsetTopAndBottom

    使用方法同上几乎一致

     //直接在onTouchEvent中调用,替换上面的layout(...)部分
           offsetLeftAndRight(offestX);
           offsetTopAndBottom(offestY); 
  • LayoutParams

    这个方式在平时开发中应该使用的比较多.使用也是很简单,就是修改params的某些参数

     //ViewGroup.MarginLayoutParams layoutParams = 
           //               (ViewGroup.MarginLayoutParams) getLayoutParams();
           //LinearLayout.LayoutParams extends ViewGroup.MarginLayoutParams,
           //几乎所有的LayoutParms都是继承至
           //ViewGroup.MarginLayoutParams,
           //所以ViewGroup.MarginLayoutParams是通用的...
           LinearLayout.LayoutParams layoutParams = 
                                 (LinearLayout.LayoutParams) getLayoutParams();
           layoutParams.leftMargin = getLeft() + offsetX;
           layoutParams.topMargin = getTop() + offsetY;
           setLayoutParams(layoutParams);
           //requestLayout();效果和上面这一句一样 
  • ViewDragHelper

    ViewDragHelper的使用过程其实也是比较简单的,主要用户控制部分都在Callback中.CallBack中的函数比较多

    Android开发艺术探索-第三章-View的事件体系

    CallBack

    下面是一个简单的栗子:

     //初始化
           mDragHelper = ViewDragHelper.create(this/*要处理的ViewGroup*/, 
                          1.0f/*敏感度*/, new DragHelperCallback()/*前面说的Callback*/);
    
           //复写一些函数,代码几乎固定
           @Override
           public boolean onInterceptTouchEvent(MotionEvent ev) {
             final int action = MotionEventCompat.getActionMasked(ev);
             if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                 mDragHelper.cancel();
                 return false;
             }
             return mDragHelper.shouldInterceptTouchEvent(ev);
           }
           @Override
           public boolean onTouchEvent(MotionEvent ev) {
             mDragHelper.processTouchEvent(ev);
             return true;
           } 
  • ScrollTo和ScrollBy

    根据函数名称就知道这两个函数的区别,To是到具体的点,by只是与当前的偏移.
    这两个函数不是针对view本身,而是针对其内容,具体来说就是ViewGroup调用这两函数,是其内部的view在移动,view调用是其内容在动(TextView-->文本,ImageView-->图像)
    另一方面就是他的参数不同与其他,正数X往左,正数Y往上.原因查看这里, 如果想要移动View,就需要在她的parent上调用这函数,下面是个栗子

     //替换上文onTouchEvent中的layout(...)
         ((ViewGroup) getParent()).scrollBy(-offsetX, -offsetY); 
  • Scroller

    在以前都不知道有这个类,哎,基础不够诶.下面一个栗子说明

     //初始化,还可以使用插值器
         Scroller mScroller = new Scroller(mContext,interpolator/*插值器,可以不用*/);
    
         //View的computescroll()
         @Override
         public void computeScroll() {
             super.computeScroll();
             // 判断Scroller是否执行完毕
             if (mScroller.computeScrollOffset()) {
                 ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
                 // 通过重绘来不断调用computeScroll
                 invalidate();//很重要
             }
         }
    
         //启动
         mScroller.startScroll(startX,startY,dX,dY,duration); 

    本质上Scroller不能移动View,在我看来她同属性动画中的ValueAnimator是一样的,因为他们都只是按照某种插值器产生数值,需要自己把数值同移动
    相联系.

3.3 View的事件分发机制

  1. 事件分发过程的三个重要方法

    • dispatchTouchEvent

      函数原型

       public boolean dispatchTouchEvent(MotionEvent ev) 

      主要的功能是负责事件的分发.
      返回值:
      true: 表示向下分发中断
      false: 表示继续向下分发

    • onInterceptTouchEvent

      函数原型

       public boolean onInterceptTouchEvent(MotionEvent event) 

      主要功能是负责事件的拦截
      返回值:
      true:拦截,事件交由自己(View/ViewGroup)的onTouchEvent(...)处理
      false:不拦截,事件继续向下分发.

    • onTouchEvent

      函数原型

       public boolean onTouchEvent(MotionEvent event) 

      主要功能是处理触摸事件
      返回值:
      true:表示消费了这个事件.
      false:表示没有消费该事件,返回到上级处理.如果一直得不到处理,最终反馈到Activity的onTouchEvent(...)

  2. 函数之间的逻辑关系

    • 以上三个函数的伪代码

      类似于递归调用的方式

       public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean consume = false;
            if (onInterceptTouchEvent(ev)) {
                consume = onTouchEvent(ev);
            } else {
                consume = child.dispatchTouchEvent(ev);
            }
            return consume;
        } 
    • 函数与监听接口

      在通常情况下,我们为Button等组件设置了onClickListener接口,有时也会设置onTouchListener接口,但在什么时候接口中的方法才会执行呢?如果设置了onTouchListener接口监听,会对View(ViewGroup)的onTouchEvent有一定的影响.如果设置了onTouchListener,她的onTouch的返回值会影响view中onTouchEvent的调用与否,onTouch返回值的含义与onTouchEvent一样,表示是否消费了该事件.onTouch会先于onTouchEvent执行.伪代码为

       //true表示消费掉
         if(!listener.onTouch(ev)){
             onTouchEvent(ev);
         } 

      对于onClickListener接口,他内部方法onCLick的调用是在onTouchEvent中(根据上面就知道如果在onTouchListener的onTouch中返回true,onclick就不会再执行了),其内部部分代码如下.

       //View#onTouchEvent(...)
         if (mPerformClick == null) {
            mPerformClick = new PerformClick();
         }
         if (!post(mPerformClick)) {
            performClick();
         }
      
         //点击事件的处理者 
         private final class PerformClick implements Runnable {
            @Override
            public void run() {
                performClick();
            }
        }
      
        //点击调用onClick函数
        public boolean performClick() {
            //ListenerInfo封装了各种监听
            final ListenerInfo li = mListenerInfo;
            if (...) {
                //调用部分
                li.mOnClickListener.onClick(this);
                result = true;
            }
            ...
            return result;
        } 

      根据上面的描述,知道调用顺序为onTouchListener#onTouch,返回值决定是否继续执行view的onTouchEvent,最后在onTouchEvent中执行onClickListener的onClick方法.

  3. 分发过程

    • Activity分发

      触摸事件最先到达Activity,所以首先会在Activity中分发

       //Activity#dispatchTouchEvent()
             public boolean dispatchTouchEvent(MotionEvent ev) {
                 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                     onUserInteraction();
                 }
                 //分发到Window.
                 if (getWindow().superDispatchTouchEvent(ev)) {
                     //true表示不再向下分发
                     return true;
                 }
                 return onTouchEvent(ev);
             } 

      在getWindow()中返回mWindow,最终在函数attach(...)中发现

       mWindow = new PhoneWindow(this); 

      PhoneWindow不在SDK中,在在线源码(Android源码)网站上可以找到相关的代码

       public boolean superDispatchTouchEvent(MotionEvent event ) {
                 //DecorView extends FrameLayout 
                 //       DecorView#superDispatchTouchEvent(ev)
                 //       public boolean superDispatchTouchEvent(MotionEvent event) {
                 //               //来到了ViewGroup
                 //               return super.dispatchTouchEvent(event);
                 //       }
                 return mDecorView.superDispatchTouchEvent(event);
             } 

      由此就把事件分发到了ViewGroup,接下来就是在VieGroup中分发.

  • View分发

    函数dispatchTouchEvent(...)中的部分代码

     ...
           if (onFilterTouchEventForSecurity(event)) {
               //noinspection SimplifiableIfStatement
               ListenerInfo li = mListenerInfo;
               if (li != null && li.mOnTouchListener != null
                       && (mViewFlags & ENABLED_MASK) == ENABLED
                       && li.mOnTouchListener.onTouch(this, event)) {
                   result = true;
               }
               // result==true,函数onTouchEvent(...)就执行不到了,而影想result的主要就是
               //li.mOnTouchListener.onTouch(this, // event)的返回值,返回true,
               //表示事件被处理了,自然不需要在调用onTouchEvent(...)来重新处理
               // 前面说过onClick(...)是在onTouchEvent(...)中调用的.即优先级小于onTouch()
               if (!result && onTouchEvent(event)) {
                   result = true;
               }
           }
           ... 

    函数onTouchEvent(...)主要就是处理事件,前面已经说过onClick的执行过程了.这里就不说了.

  • ViewGroup分发

    函数dispatchTouchEvent(...)中的部分代码

     // Check for interception.
           final boolean intercepted;
           // 事件为ACTION_DOWN或者mFirstTouchTarget不为null
           //(即已经找到能够接收touch事件的目标组件)时if成立
           if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
               //判断disallowIntercept(禁止拦截)标志位
               //因为在其他地方可能调用了
               //requestDisallowInterceptTouchEvent(boolean disallowIntercept)
               //从而禁止执行是否需要拦截的判断
               //(有点拗口~其实看requestDisallowInterceptTouchEvent()方法名就可明白)
               final boolean disallowIntercept = 
                                  (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
               //补充:根据下面的代码可以发现, disallowIntercept 的值等于函数
               //requestDisallowInterceptTouchEvent的参数.                 
               if (!disallowIntercept) {
                   intercepted = onInterceptTouchEvent(ev);
                   ev.setAction(action); // restore action in case it was changed
               } else {
                   intercepted = false;
               }
           } else {
               // There are no touch targets and this action is not an initial down
               // so this view group continues to intercept touches.
               intercepted = true;
           } 

    注意上文代码中的注释部分,这里看一下部分requesrDisallowInterceptTouchEvent(...)的部分源码

     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                   //更具这里可以看出,当disallowIntercept=true时,
                   //(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 成立,
                   //这就意味着上面一段代码中的disallowIntercept=true;
                   if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                       // We're already in this state, assume our ancestors are too
                       return;
                   }
                   ...
          } 

    由此可见VIewGroup只会在ACTION=ACTION_DOWN或者mFirstTouchTarget != null时才判断是否拦截事件,因为一个事件序列(DOWN->MOVE->...->UP)只能有一个View处理.但是mFirstTouchTarget != null表示什么呢?

    当事件被ViewGroup的子元素成功处理了(子View的onTouchEvent/onTouch返回了true??),mFirstTouchTarget被赋值指向子元素(即!=null)

    函数dispatchTouchEvent(...)的部分实现.

     final View[] children = mChildren;
       for (int i = childrenCount - 1; i >= 0; i--) {
           final int childIndex = customOrder
                   ? getChildDrawingOrder(childrenCount, i) : i;
           final View child = (preorderedList == null)
                   ? children[childIndex] : preorderedList.get(childIndex);
           // If there is a view that has accessibility focus we want it
           // to get the event first and if not handled we will perform a
           // normal dispatch. We may do a double iteration but this is
           // safer given the timeframe.
           if (childWithAccessibilityFocus != null) {
               if (childWithAccessibilityFocus != child) {
                   continue;
               }
               childWithAccessibilityFocus = null;
               i = childrenCount - 1;
           }
    
           if (!canViewReceivePointerEvents(child)
                   || !isTransformedTouchPointInView(x, y, child, null)) {
               ev.setTargetAccessibilityFocus(false);
               continue;
           }
           newTouchTarget = getTouchTarget(child);
           if (newTouchTarget != null) {
               // Child is already receiving touch within its bounds.
               // Give it the new pointer in addition to the ones it is handling.
               // 找到接收Touch事件的子View!!!!!!!即为newTouchTarget.
               newTouchTarget.pointerIdBits |= idBitsToAssign;
               break;
           }
    
           resetCancelNextUpFlag(child);
           //注意这个方法,再后面再看看..根据源码,
           //可以知道它返回的是子View(child)的dispatchTouchEvent(...)
           //当child==null,返回super.dispatchTouchEvent(...),
           //即View的dispatchTouchEvent(...)
           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
               // Child wants to receive touch within its bounds.
               mLastTouchDownTime = ev.getDownTime();
               if (preorderedList != null) {
                   // childIndex points into presorted list, find original index
                   for (int j = 0; j < childrenCount; j++) {
                       if (children[childIndex] == mChildren[j]) {
                           mLastTouchDownIndex = j;
                           break;
                       }
                   }
               } else {
                   mLastTouchDownIndex = childIndex;
               }
               mLastTouchDownX = ev.getX();
               mLastTouchDownY = ev.getY();
               //找到了事件的处理者,终止循环
               newTouchTarget = addTouchTarget(child, idBitsToAssign);
               alreadyDispatchedToNewTouchTarget = true;
               break;
           }
    
           // The accessibility focus didn't handle the event, so clear
           // the flag and do a normal dispatch to all children.
           ev.setTargetAccessibilityFocus(false);
       } 

同样是dispatchTouchEvent(...)的部分代码

 // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //这里说明没有子View处理该事件,只得有View的dispatchTouchEvent(...)来处理.
                //关于该函数的部分源码在后面介绍.
                handled = dispatchTransformedTouchEvent(ev, canceled, null/*child*/,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
             ...   
            }

 函数addTouchTarget(...)的具体实现.

            private TouchTarget addTouchTarget(View child, int pointerIdBits) {
                TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
                target.next = mFirstTouchTarget;
                mFirstTouchTarget = target;
                return target;
            }

  函数dispatchTransformedTouchEvent(...)的部分实现.

        ....
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ....
        return handled. 

3.4 View的滑动冲突

  • 常见的滑动冲突的场景:

    1. 外部滑动方向和内部滑动方向不一致,例如viewpager中包含listview;
    2. 外部滑动方向和内部滑动方向一致,例如viewpager的单页中存在可以滑动的bannerview;
    3. 上面两种情况的嵌套,例如viewpager的单个页面中包含了bannerview和listview。
  • 滑动冲突处理规则

    可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等

  • 解决方式

    1. 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,其他均不需要做修改。伪代码如下:

       public boolean onInterceptTouchEvent(MotionEvent event) {
           boolean intercepted = false;
           int x = (int) event.getX();
           int y = (int) event.getY();
      
           switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN: {
               intercepted = false;
               break;
           }
           case MotionEvent.ACTION_MOVE: {
               int deltaX = x - mLastXIntercept;
               int deltaY = y - mLastYIntercept;
               if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
                   intercepted = true;
               } else {
                   intercepted = false;
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               intercepted = false;
               break;
           }
           default:
               break;
           }
      
           mLastXIntercept = x;
           mLastYIntercept = y;
      
           return intercepted;
       } 
    2. 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

       public boolean dispatchTouchEvent(MotionEvent event) {
             int x = (int) event.getX();
             int y = (int) event.getY();
      
             switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN: {
                 getParent().requestDisallowInterceptTouchEvent(true);
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
                 int deltaX = x - mLastX;
                 int deltaY = y - mLastY;
                 if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
                     getParent().requestDisallowInterceptTouchEvent(false);
                 }
                 break;
             }
             case MotionEvent.ACTION_UP: {
                 break;
             }
             default:
                 break;
             }
             mLastX = x;
             mLastY = y;
             return super.dispatchTouchEvent(event);
         } 

      父View的onInterceptTouchEvent(...)伪代码

       public boolean  onInterceptTouchEvent(MotionEvent ev){
             if(ev.getAction() == MotionEvent.ACTION_DOWN){
                 retuen false;
             }else{
                 retuen true;
             }
         } 

      内部拦截法过程说明,父类在ACTION_DOWN时不拦截,子类在ACTION_DOWN时拦截,这时mFirstTouchTarget!=null, disallowIntercept = true,这意味着父类的onInterceptTouchEvent(...)不会再被执行,并且一个事件序列只有一个View来处理,则所有的后续ACTION_MOVE都会传到子View,当在子View中判断到某个事件应该由父View处理,只需重置disallowIntercept=false即可,即调用函数requestDisallowInterceptTouchEvent(false),这时事件就到父View的onTouchEvent(...)处理的(因为onInterceptionTouchEvent在非ACTION_DOWN时都返回true).如果父类没有在设置requestDisallowInterceptTouchEvent(true)的话,这个事件就会一直都在父View中做处理了.(注:为个人理解,若有不对,望其指出)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Android蓝牙连接汽车OBD设备
//设备连接public class BluetoothConnect implements Runnable {    private static final UUID CONNECT_UUID  UUID.fromString("0000110100001000800000805F9B34FB");
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这