zoukankan      html  css  js  c++  java
  • 自定义实现ExpandableListView收缩的简单动画效果

        以下是 ExpandableListView 收缩的简单动画效果

      1 /*
      2  * Copyright (C) 2014 Gary Guo
      3  * 
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  * 
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  * 
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16  
     17 package com.idunnololz.widgets;
     18 
     19 import java.util.ArrayList;
     20 import java.util.List;
     21 
     22 import android.annotation.SuppressLint;
     23 import android.content.Context;
     24 import android.graphics.Canvas;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Build;
     27 import android.util.AttributeSet;
     28 import android.util.SparseArray;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.animation.Animation;
     32 import android.view.animation.Animation.AnimationListener;
     33 import android.view.animation.Transformation;
     34 import android.widget.AbsListView;
     35 import android.widget.BaseExpandableListAdapter;
     36 import android.widget.ExpandableListAdapter;
     37 import android.widget.ExpandableListView;
     38 
     39 /**
     40  * This class defines an ExpandableListView which supports animations for
     41  * collapsing and expanding groups.
     42  */
     43 public class AnimatedExpandableListView extends ExpandableListView {
     44     /*
     45      * A detailed explanation for how this class works:
     46      *
     47      * Animating the ExpandableListView was no easy task. The way that this
     48      * class does it is by exploiting how an ExpandableListView works.
     49      *
     50      * Normally when {@link ExpandableListView#collapseGroup(int)} or
     51      * {@link ExpandableListView#expandGroup(int)} is called, the view toggles
     52      * the flag for a group and calls notifyDataSetChanged to cause the ListView
     53      * to refresh all of it's view. This time however, depending on whether a
     54      * group is expanded or collapsed, certain childViews will either be ignored
     55      * or added to the list.
     56      *
     57      * Knowing this, we can come up with a way to animate our views. For
     58      * instance for group expansion, we tell the adapter to animate the
     59      * children of a certain group. We then expand the group which causes the
     60      * ExpandableListView to refresh all views on screen. The way that
     61      * ExpandableListView does this is by calling getView() in the adapter.
     62      * However since the adapter knows that we are animating a certain group,
     63      * instead of returning the real views for the children of the group being
     64      * animated, it will return a fake dummy view. This dummy view will then
     65      * draw the real child views within it's dispatchDraw function. The reason
     66      * we do this is so that we can animate all of it's children by simply
     67      * animating the dummy view. After we complete the animation, we tell the
     68      * adapter to stop animating the group and call notifyDataSetChanged. Now
     69      * the ExpandableListView is forced to refresh it's views again, except this
     70      * time, it will get the real views for the expanded group.
     71      *
     72      * So, to list it all out, when {@link #expandGroupWithAnimation(int)} is
     73      * called the following happens:
     74      *
     75      * 1. The ExpandableListView tells the adapter to animate a certain group.
     76      * 2. The ExpandableListView calls expandGroup.
     77      * 3. ExpandGroup calls notifyDataSetChanged.
     78      * 4. As an result, getChildView is called for expanding group.
     79      * 5. Since the adapter is in "animating mode", it will return a dummy view.
     80      * 6. This dummy view draws the actual children of the expanding group.
     81      * 7. This dummy view's height is animated from 0 to it's expanded height.
     82      * 8. Once the animation completes, the adapter is notified to stop
     83      *    animating the group and notifyDataSetChanged is called again.
     84      * 9. This forces the ExpandableListView to refresh all of it's views again.
     85      * 10.This time when getChildView is called, it will return the actual
     86      *    child views.
     87      *
     88      * For animating the collapse of a group is a bit more difficult since we
     89      * can't call collapseGroup from the start as it would just ignore the
     90      * child items, giving up no chance to do any sort of animation. Instead
     91      * what we have to do is play the animation first and call collapseGroup
     92      * after the animation is done.
     93      *
     94      * So, to list it all out, when {@link #collapseGroupWithAnimation(int)} is
     95      * called the following happens:
     96      *
     97      * 1. The ExpandableListView tells the adapter to animate a certain group.
     98      * 2. The ExpandableListView calls notifyDataSetChanged.
     99      * 3. As an result, getChildView is called for expanding group.
    100      * 4. Since the adapter is in "animating mode", it will return a dummy view.
    101      * 5. This dummy view draws the actual children of the expanding group.
    102      * 6. This dummy view's height is animated from it's current height to 0.
    103      * 7. Once the animation completes, the adapter is notified to stop
    104      *    animating the group and notifyDataSetChanged is called again.
    105      * 8. collapseGroup is finally called.
    106      * 9. This forces the ExpandableListView to refresh all of it's views again.
    107      * 10.This time when the ListView will not get any of the child views for
    108      *    the collapsed group.
    109      */
    110 
    111     @SuppressWarnings("unused")
    112     private static final String TAG = AnimatedExpandableListAdapter.class.getSimpleName();
    113 
    114     /**
    115      * The duration of the expand/collapse animations
    116      */
    117     private static final int ANIMATION_DURATION = 100;
    118 
    119     private AnimatedExpandableListAdapter adapter;
    120 
    121     public AnimatedExpandableListView(Context context) {
    122         super(context);
    123     }
    124 
    125     public AnimatedExpandableListView(Context context, AttributeSet attrs) {
    126         super(context, attrs);
    127     }
    128 
    129     public AnimatedExpandableListView(Context context, AttributeSet attrs, int defStyle) {
    130         super(context, attrs, defStyle);
    131     }
    132 
    133     /**
    134      * @see ExpandableListView#setAdapter(ExpandableListAdapter)
    135      */
    136     public void setAdapter(ExpandableListAdapter adapter) {
    137         super.setAdapter(adapter);
    138 
    139         // Make sure that the adapter extends AnimatedExpandableListAdapter
    140         if(adapter instanceof AnimatedExpandableListAdapter) {
    141             this.adapter = (AnimatedExpandableListAdapter) adapter;
    142             this.adapter.setParent(this);
    143         } else {
    144             throw new ClassCastException(adapter.toString() + " must implement AnimatedExpandableListAdapter");
    145         }
    146     }
    147 
    148     /**
    149      * Expands the given group with an animation.
    150      * @param groupPos The position of the group to expand
    151      * @return  Returns true if the group was expanded. False if the group was
    152      *          already expanded.
    153      */
    154     @SuppressLint("NewApi") 
    155     public boolean expandGroupWithAnimation(int groupPos) {
    156         boolean lastGroup = groupPos == adapter.getGroupCount() - 1;
    157         if (lastGroup && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    158             return expandGroup(groupPos, true);
    159         }
    160         
    161         int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos));
    162         if (groupFlatPos != -1) {
    163             int childIndex = groupFlatPos - getFirstVisiblePosition();
    164             if (childIndex < getChildCount()) {
    165                 // Get the view for the group is it is on screen...
    166                 View v = getChildAt(childIndex);
    167                 if (v.getBottom() >= getBottom()) {
    168                     // If the user is not going to be able to see the animation
    169                     // we just expand the group without an animation.
    170                     // This resolves the case where getChildView will not be
    171                     // called if the children of the group is not on screen
    172 
    173                     // We need to notify the adapter that the group was expanded
    174                     // without it's knowledge
    175                     adapter.notifyGroupExpanded(groupPos);
    176                     return expandGroup(groupPos);
    177                 }
    178             }
    179         }
    180 
    181         // Let the adapter know that we are starting the animation...
    182         adapter.startExpandAnimation(groupPos, 0);
    183         // Finally call expandGroup (note that expandGroup will call
    184         // notifyDataSetChanged so we don't need to)
    185         return expandGroup(groupPos);
    186     }
    187 
    188     /**
    189      * Collapses the given group with an animation.
    190      * @param groupPos The position of the group to collapse
    191      * @return  Returns true if the group was collapsed. False if the group was
    192      *          already collapsed.
    193      */
    194     public boolean collapseGroupWithAnimation(int groupPos) {
    195         int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos));
    196         if (groupFlatPos != -1) {
    197             int childIndex = groupFlatPos - getFirstVisiblePosition();
    198             if (childIndex >= 0 && childIndex < getChildCount()) {
    199                 // Get the view for the group is it is on screen...
    200                 View v = getChildAt(childIndex);
    201                 if (v.getBottom() >= getBottom()) {
    202                     // If the user is not going to be able to see the animation
    203                     // we just collapse the group without an animation.
    204                     // This resolves the case where getChildView will not be
    205                     // called if the children of the group is not on screen
    206                     return collapseGroup(groupPos);
    207                 }
    208             } else {
    209                 // If the group is offscreen, we can just collapse it without an
    210                 // animation...
    211                 return collapseGroup(groupPos);
    212             }
    213         }
    214 
    215         // Get the position of the firstChild visible from the top of the screen
    216         long packedPos = getExpandableListPosition(getFirstVisiblePosition());
    217         int firstChildPos = getPackedPositionChild(packedPos);
    218         int firstGroupPos = getPackedPositionGroup(packedPos);
    219 
    220         // If the first visible view on the screen is a child view AND it's a
    221         // child of the group we are trying to collapse, then set that
    222         // as the first child position of the group... see
    223         // {@link #startCollapseAnimation(int, int)} for why this is necessary
    224         firstChildPos = firstChildPos == -1 || firstGroupPos != groupPos ? 0 : firstChildPos;
    225 
    226         // Let the adapter know that we are going to start animating the
    227         // collapse animation.
    228         adapter.startCollapseAnimation(groupPos, firstChildPos);
    229 
    230         // Force the listview to refresh it's views
    231         adapter.notifyDataSetChanged();
    232         return isGroupExpanded(groupPos);
    233     }
    234 
    235     private int getAnimationDuration() {
    236         return ANIMATION_DURATION;
    237     }
    238 
    239     /**
    240      * Used for holding information regarding the group.
    241      */
    242     private static class GroupInfo {
    243         boolean animating = false;
    244         boolean expanding = false;
    245         int firstChildPosition;
    246 
    247         /**
    248          * This variable contains the last known height value of the dummy view.
    249          * We save this information so that if the user collapses a group
    250          * before it fully expands, the collapse animation will start from the
    251          * CURRENT height of the dummy view and not from the full expanded
    252          * height.
    253          */
    254         int dummyHeight = -1;
    255     }
    256 
    257     /**
    258      * A specialized adapter for use with the AnimatedExpandableListView. All
    259      * adapters used with AnimatedExpandableListView MUST extend this class.
    260      */
    261     public static abstract class AnimatedExpandableListAdapter extends BaseExpandableListAdapter {
    262         private SparseArray<GroupInfo> groupInfo = new SparseArray<GroupInfo>();
    263         private AnimatedExpandableListView parent;
    264 
    265         private static final int STATE_IDLE = 0;
    266         private static final int STATE_EXPANDING = 1;
    267         private static final int STATE_COLLAPSING = 2;
    268 
    269         private void setParent(AnimatedExpandableListView parent) {
    270             this.parent = parent;
    271         }
    272 
    273         public int getRealChildType(int groupPosition, int childPosition) {
    274             return 0;
    275         }
    276 
    277         public int getRealChildTypeCount() {
    278             return 1;
    279         }
    280 
    281         public abstract View getRealChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent);
    282         public abstract int getRealChildrenCount(int groupPosition);
    283 
    284         private GroupInfo getGroupInfo(int groupPosition) {
    285             GroupInfo info = groupInfo.get(groupPosition);
    286             if (info == null) {
    287                 info = new GroupInfo();
    288                 groupInfo.put(groupPosition, info);
    289             }
    290             return info;
    291         }
    292 
    293         public void notifyGroupExpanded(int groupPosition) {
    294             GroupInfo info = getGroupInfo(groupPosition);
    295             info.dummyHeight = -1;
    296         }
    297 
    298         private void startExpandAnimation(int groupPosition, int firstChildPosition) {
    299             GroupInfo info = getGroupInfo(groupPosition);
    300             info.animating = true;
    301             info.firstChildPosition = firstChildPosition;
    302             info.expanding = true;
    303         }
    304 
    305         private void startCollapseAnimation(int groupPosition, int firstChildPosition) {
    306             GroupInfo info = getGroupInfo(groupPosition);
    307             info.animating = true;
    308             info.firstChildPosition = firstChildPosition;
    309             info.expanding = false;
    310         }
    311 
    312         private void stopAnimation(int groupPosition) {
    313             GroupInfo info = getGroupInfo(groupPosition);
    314             info.animating = false;
    315         }
    316 
    317         /**
    318          * Override {@link #getRealChildType(int, int)} instead.
    319          */
    320         @Override
    321         public final int getChildType(int groupPosition, int childPosition) {
    322             GroupInfo info = getGroupInfo(groupPosition);
    323             if (info.animating) {
    324                 // If we are animating this group, then all of it's children
    325                 // are going to be dummy views which we will say is type 0.
    326                 return 0;
    327             } else {
    328                 // If we are not animating this group, then we will add 1 to
    329                 // the type it has so that no type id conflicts will occur
    330                 // unless getRealChildType() returns MAX_INT
    331                 return getRealChildType(groupPosition, childPosition) + 1;
    332             }
    333         }
    334 
    335         /**
    336          * Override {@link #getRealChildTypeCount()} instead.
    337          */
    338         @Override
    339         public final int getChildTypeCount() {
    340             // Return 1 more than the childTypeCount to account for DummyView
    341             return getRealChildTypeCount() + 1;
    342         }
    343         
    344         protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
    345             return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    346                                                 ViewGroup.LayoutParams.WRAP_CONTENT, 0);
    347         }
    348 
    349         /**
    350          * Override {@link #getChildView(int, int, boolean, View, ViewGroup)} instead.
    351          */
    352         @Override
    353         public final View getChildView(final int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
    354             final GroupInfo info = getGroupInfo(groupPosition);
    355 
    356             if (info.animating) {
    357                 // If this group is animating, return the a DummyView...
    358                 if (convertView instanceof DummyView == false) {
    359                     convertView = new DummyView(parent.getContext());
    360                     convertView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, 0));
    361                 }
    362 
    363                 if (childPosition < info.firstChildPosition) {
    364                     // The reason why we do this is to support the collapse
    365                     // this group when the group view is not visible but the
    366                     // children of this group are. When notifyDataSetChanged
    367                     // is called, the ExpandableListView tries to keep the
    368                     // list position the same by saving the first visible item
    369                     // and jumping back to that item after the views have been
    370                     // refreshed. Now the problem is, if a group has 2 items
    371                     // and the first visible item is the 2nd child of the group
    372                     // and this group is collapsed, then the dummy view will be
    373                     // used for the group. But now the group only has 1 item
    374                     // which is the dummy view, thus when the ListView is trying
    375                     // to restore the scroll position, it will try to jump to
    376                     // the second item of the group. But this group no longer
    377                     // has a second item, so it is forced to jump to the next
    378                     // group. This will cause a very ugly visual glitch. So
    379                     // the way that we counteract this is by creating as many
    380                     // dummy views as we need to maintain the scroll position
    381                     // of the ListView after notifyDataSetChanged has been
    382                     // called.
    383                     convertView.getLayoutParams().height = 0;
    384                     return convertView;
    385                 }
    386 
    387                 final ExpandableListView listView = (ExpandableListView) parent;
    388 
    389                 final DummyView dummyView = (DummyView) convertView;
    390 
    391                 // Clear the views that the dummy view draws.
    392                 dummyView.clearViews();
    393 
    394                 // Set the style of the divider
    395                 dummyView.setDivider(listView.getDivider(), parent.getMeasuredWidth(), listView.getDividerHeight());
    396 
    397                 // Make measure specs to measure child views
    398                 final int measureSpecW = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY);
    399                 final int measureSpecH = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    400 
    401                 int totalHeight = 0;
    402                 int clipHeight = parent.getHeight();
    403 
    404                 final int len = getRealChildrenCount(groupPosition);
    405                 for (int i = info.firstChildPosition; i < len; i++) {
    406                     View childView = getRealChildView(groupPosition, i, (i == len - 1), null, parent);
    407                     
    408                     LayoutParams p = (LayoutParams) childView.getLayoutParams();
    409                     if (p == null) {
    410                         p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
    411                         childView.setLayoutParams(p);
    412                     }
    413                     
    414                     int lpHeight = p.height;
    415                     
    416                     int childHeightSpec;
    417                     if (lpHeight > 0) {
    418                         childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
    419                     } else {
    420                         childHeightSpec = measureSpecH;
    421                     }
    422                     
    423                     childView.measure(measureSpecW, childHeightSpec);
    424                     totalHeight += childView.getMeasuredHeight();
    425                     
    426                     if (totalHeight < clipHeight) {
    427                         // we only need to draw enough views to fool the user...
    428                         dummyView.addFakeView(childView);
    429                     } else {
    430                         dummyView.addFakeView(childView);
    431                         
    432                         // if this group has too many views, we don't want to
    433                         // calculate the height of everything... just do a light
    434                         // approximation and break
    435                         int averageHeight = totalHeight / (i + 1);
    436                         totalHeight += (len - i - 1) * averageHeight;
    437                         break;
    438                     }
    439                 }
    440 
    441                 Object o;
    442                 int state = (o = dummyView.getTag()) == null ? STATE_IDLE : (Integer) o;
    443 
    444                 if (info.expanding && state != STATE_EXPANDING) {
    445                     ExpandAnimation ani = new ExpandAnimation(dummyView, 0, totalHeight, info);
    446                     ani.setDuration(this.parent.getAnimationDuration());
    447                     ani.setAnimationListener(new AnimationListener() {
    448 
    449                         @Override
    450                         public void onAnimationEnd(Animation animation) {
    451                             stopAnimation(groupPosition);
    452                             notifyDataSetChanged();
    453                             dummyView.setTag(STATE_IDLE);
    454                         }
    455 
    456                         @Override
    457                         public void onAnimationRepeat(Animation animation) {}
    458 
    459                         @Override
    460                         public void onAnimationStart(Animation animation) {}
    461 
    462                     });
    463                     dummyView.startAnimation(ani);
    464                     dummyView.setTag(STATE_EXPANDING);
    465                 } else if (!info.expanding && state != STATE_COLLAPSING) {
    466                     if (info.dummyHeight == -1) {
    467                         info.dummyHeight = totalHeight;
    468                     }
    469 
    470                     ExpandAnimation ani = new ExpandAnimation(dummyView, info.dummyHeight, 0, info);
    471                     ani.setDuration(this.parent.getAnimationDuration());
    472                     ani.setAnimationListener(new AnimationListener() {
    473 
    474                         @Override
    475                         public void onAnimationEnd(Animation animation) {
    476                             stopAnimation(groupPosition);
    477                             listView.collapseGroup(groupPosition);
    478                             notifyDataSetChanged();
    479                             info.dummyHeight = -1;
    480                             dummyView.setTag(STATE_IDLE);
    481                         }
    482 
    483                         @Override
    484                         public void onAnimationRepeat(Animation animation) {}
    485 
    486                         @Override
    487                         public void onAnimationStart(Animation animation) {}
    488 
    489                     });
    490                     dummyView.startAnimation(ani);
    491                     dummyView.setTag(STATE_COLLAPSING);
    492                 }
    493 
    494                 return convertView;
    495             } else {
    496                 return getRealChildView(groupPosition, childPosition, isLastChild, convertView, parent);
    497             }
    498         }
    499 
    500         @Override
    501         public final int getChildrenCount(int groupPosition) {
    502             GroupInfo info = getGroupInfo(groupPosition);
    503             if (info.animating) {
    504                 return info.firstChildPosition + 1;
    505             } else {
    506                 return getRealChildrenCount(groupPosition);
    507             }
    508         }
    509 
    510     }
    511 
    512     private static class DummyView extends View {
    513         private List<View> views = new ArrayList<View>();
    514         private Drawable divider;
    515         private int dividerWidth;
    516         private int dividerHeight;
    517 
    518         public DummyView(Context context) {
    519             super(context);
    520         }
    521 
    522         public void setDivider(Drawable divider, int dividerWidth, int dividerHeight) {
    523             if(divider != null) {
    524                 this.divider = divider;
    525                 this.dividerWidth = dividerWidth;
    526                 this.dividerHeight = dividerHeight;
    527 
    528                 divider.setBounds(0, 0, dividerWidth, dividerHeight);
    529             }
    530         }
    531 
    532         /**
    533          * Add a view for the DummyView to draw.
    534          * @param childView View to draw
    535          */
    536         public void addFakeView(View childView) {
    537             childView.layout(0, 0, getWidth(), childView.getMeasuredHeight());
    538             views.add(childView);
    539         }
    540         
    541         @Override
    542         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    543             super.onLayout(changed, left, top, right, bottom);
    544             final int len = views.size();
    545             for(int i = 0; i < len; i++) {
    546                 View v = views.get(i);
    547                 v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
    548             }
    549         }
    550 
    551         public void clearViews() {
    552             views.clear();
    553         }
    554 
    555         @Override
    556         public void dispatchDraw(Canvas canvas) {
    557             canvas.save();
    558             if(divider != null) {
    559                 divider.setBounds(0, 0, dividerWidth, dividerHeight);
    560             }
    561             
    562             final int len = views.size();
    563             for(int i = 0; i < len; i++) {
    564                 View v = views.get(i);
    565                 
    566                 canvas.save();
    567                 canvas.clipRect(0, 0, getWidth(), v.getMeasuredHeight());
    568                 v.draw(canvas);
    569                 canvas.restore();
    570                 
    571                 if(divider != null) {
    572                     divider.draw(canvas);
    573                     canvas.translate(0, dividerHeight);
    574                 }
    575                 
    576                 canvas.translate(0, v.getMeasuredHeight());
    577             }
    578             
    579             canvas.restore();
    580         }
    581     }
    582 
    583     private static class ExpandAnimation extends Animation {
    584         private int baseHeight;
    585         private int delta;
    586         private View view;
    587         private GroupInfo groupInfo;
    588 
    589         private ExpandAnimation(View v, int startHeight, int endHeight, GroupInfo info) {
    590             baseHeight = startHeight;
    591             delta = endHeight - startHeight;
    592             view = v;
    593             groupInfo = info;
    594 
    595             view.getLayoutParams().height = startHeight;
    596             view.requestLayout();
    597         }
    598 
    599         @Override
    600         protected void applyTransformation(float interpolatedTime, Transformation t) {
    601             super.applyTransformation(interpolatedTime, t);
    602             if (interpolatedTime < 1.0f) {
    603                 int val = baseHeight + (int) (delta * interpolatedTime);
    604                 view.getLayoutParams().height = val;
    605                 groupInfo.dummyHeight = val;
    606                 view.requestLayout();
    607             } else {
    608                 int val = baseHeight + delta;
    609                 view.getLayoutParams().height = val;
    610                 groupInfo.dummyHeight = val;
    611                 view.requestLayout();
    612             }
    613         }
    614     }
    615 }
  • 相关阅读:
    PVLAN 简介
    SFP光模块与SFP+、XFP、QSFP、GBIC、BIDI的区别
    IP防护等级简介
    工业交换机和普通交换机的区别
    博客园 添加 “返回顶部” 按钮
    Linux SHELL中sh和bash的区别
    vpshere6 ESXI 禁止登陆 "执行此操作的权限被拒绝"
    python icmpdnshttp监控网络各个节点状态,并记录日志
    Python Threading问题:TypeError in Threading. function takes 1 positional argument but 100 were given
    django信号
  • 原文地址:https://www.cnblogs.com/kingfly13/p/4457260.html
Copyright © 2011-2022 走看看