`
lovehong0306
  • 浏览: 12190 次
  • 性别: Icon_minigender_1
  • 来自: 石家庄
社区版块
存档分类
最新评论

android-misc-widgets多方抽屉bug修复版 解决“闪烁”问题

阅读更多

前几天项目需要用到左侧拉出抽屉,想到了http://blog.csdn.net/hellogv/article/details/6264706中提到的多方抽屉,拿来试用了下,发现bug还真不少,最不能忍受的是最后那一下“闪烁”,于是乎,改!
下面将修改过程中遇到的问题及其解决方法分享给大家。

首先是出现了如图的情况:

当以光的速度点击handle(就是那个带箭头的Button)并拉出到很远很远的地方 就出先上边那个神奇的现象了

寻找原因,发现是这里的问题

[java] view plaincopy
  1. <span style="white-space:pre">            </span>if (tmpX != mTrackX || tmpY != mTrackY)  
  2.             {  
  3.                 mTrackX = tmpX;  
  4.                 mTrackY = tmpY;  
  5.                 // invalidate(); //放在此导致极快速滑动至touch区域外界面不刷新(mTrackX、mTrackY均为0)  
  6.             }  
  7.             invalidate();  


 

就拿上边那种情况来讲

当瞬间将handle拉至最大位置,即 tmpX=0 的位置,由于mTrackX默认为0,if条件不成立,执行不到invalidate()方法,页面没有刷新

将invalidate()方法移到if'条件语句之外即可解决问题


下一问题:onFling方法在将抽屉快速抽出时基本不能用

抽出来~滑进去~抽出来~滑进去~     (这个抽屉带弹簧的@_@?!)

究其原因,在这里

 

[java] view plaincopy
  1. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {  
  2.         mState = State.FLYING;  
  3.         mVelocity = mOrientation == VERTICAL? velocityY : velocityX;  
  4.         post(startAnimation);  
  5.         return true;  
  6.     }  

 

mVelocity 使用的是onFling方法传进来的参数velocityX,经多次打印log发现velocityX为负数大致看了下源码,这个速度是基于getX()方法算出来的是
大家都知道,getX()方法是获取以widget左上角为坐标原点计算的X轴坐标值(不知道的看这里:http://blog.csdn.net/lovehong0306/article/details/7451507
由此推想而知
1.点击handle(此时content为GONE),这时的getX()得到的是以handle左上角为原点的坐标

2.快速滑动以发动onFling方法(快到只有两个event事件发生),这时getX()得到的依然是以handle的左上角为原点的坐标,但是由于content已经可见,handle的位置发生了变化,为抽屉完全抽出时的位置,而action_up事件发生时的getX()得到是在handle原点的左边,即为负值,用此时的X坐标值减去之前得到的那个正的坐标值,结果当然是负的了

3.有负的偏移量和时间,计算出来的速度也就是负的了

这就是为什么拉出抽屉后会滑进去的原因了


修改为如下即可解决:

 

[java] view plaincopy
  1. @Override  
  2.         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,  
  3.                 float velocityY)  
  4.         {  
  5.             mState = State.FLYING;  
  6.   
  7.             float velocityX2, velocityY2;  
  8.             if (lastRawX == -1 && lastRawY == -1)   //见onScroll方法  
  9.             {  
  10.                 velocityX2 = (curRawX - e1.getRawX())  
  11.                         / (curEventTime - e1.getEventTime()) * 1000//  px/s  
  12.                 velocityY2 = (curRawY - e1.getRawY())  
  13.                         / (curEventTime - e1.getEventTime()) * 1000;  
  14.             }  
  15.             else  
  16.             {  
  17.                 velocityX2 = (curRawX - lastRawX)  
  18.                         / (curEventTime - lastEventTime) * 1000;  
  19.                 velocityY2 = (curRawY - lastRawY)  
  20.                         / (curEventTime - lastEventTime) * 1000;  
  21.             }  
  22.   
  23.             mVelocity = mOrientation == VERTICAL ? velocityY2 : velocityX2;  
  24.   
  25.             if (Math.abs(mVelocity) > 50)  
  26.             {  
  27.                 if (mVelocity > 0)  
  28.                 {  
  29.                     mAnimatedAcceleration = mMaximumAcceleration;  
  30.                 }  
  31.                 else  
  32.                 {  
  33.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  34.                 }  
  35.   
  36.                 long now = SystemClock.uptimeMillis();  
  37.                 mAnimationLastTime = now;  
  38.                 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;  
  39.                 mAnimating = true;  
  40.                 mHandler.removeMessages(MSG_ANIMATE);  
  41.                 mHandler.removeMessages(MSG_PREPARE_ANIMATE);  
  42.                 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),  
  43.                         mCurrentAnimationTime);  
  44.                 return true;  
  45.             }  
  46.             return false;  
  47.         }  

 

代码就不多做解释了,命名还算规范,应该能看懂,最后那几行是为解决“闪烁”问题的

下面就来说下最棘手的问题——“闪烁”


那么为什么会闪烁呢?

经多次尝试,发现是动画与setVisibility(GONG)冲突,当把动画设置为setFillAfter(true)后即可发现,动画结束后设置控件setVisibility(GONG),消失的不仅仅是content,handle也一同消失了。

由此可知handle在动画结束后先消失再出现,于是就出现了闪烁的效果


那么好办,只要把content和handle分别同时设置动画不就行了,content在动画结束后setVisibility(GONG),handle不setVisibility(GONG)。

But,这么尝试了一下发现,虽然“几乎”同时start动画,毕竟还是有时间间隔的,机子性能越差越明显,content和handle分开了!!!

此法行不通,另想他法


源码真是个好东西,看看SlidingDrawer是怎么实现的

原来如此,没用系统动画,利用handler重复改变控件位置

好,就按照这个思路,结合当前代码,改!

(完整代码稍后贴出)

把所有post(startAnimation)替换成:

 

[java] view plaincopy
  1. long now = SystemClock.uptimeMillis();  
  2.                 mAnimationLastTime = now;  
  3.                 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;  
  4.                 mAnimating = true;  
  5.                 mHandler.removeMessages(MSG_ANIMATE);  
  6.                 mHandler.removeMessages(MSG_PREPARE_ANIMATE);  
  7.                 mHandler.sendMessageAtTime(  
  8.                         mHandler.obtainMessage(MSG_PREPARE_ANIMATE),  
  9.                         mCurrentAnimationTime);  


这段代码基本上是从SlidingDrawer源码copy过来的,MSG_PREPARE_ANIMATE是自己加的

起初不明白ANIMATION_FRAME_DURATION的作用,为什么要延迟呢?

后来发现,这个延迟是留给onLayout方法的,如果不加这个延迟,后边用到的方法就可能在onLayout方法之前调用,也就导致了在onLayout方法之前用到了mContentWidth或者mContentHeight,此时的值为0,这也是为什么要另加MSG_PREPARE_ANIMATE


prepareAnimation方法代码如下:

 

[java] view plaincopy
  1. private void prepareAnimation()  
  2.     {  
  3.   
  4.         switch (mPosition)  
  5.         {  
  6.             case LEFT:  
  7.                 if (mIsShrinking)  
  8.                 {  
  9.                     mVelocity = -mMaximumMajorVelocity;  
  10.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  11.   
  12.                 }  
  13.                 else  
  14.                 {  
  15.                     mVelocity = mMaximumMajorVelocity;  
  16.                     mAnimatedAcceleration = mMaximumAcceleration;  
  17.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  18.                     {  
  19.                         mTrackX = -mContentWidth;  
  20.                     }  
  21.                 }  
  22.                 break;  
  23.             case RIGHT:  
  24.                 if (mIsShrinking)  
  25.                 {  
  26.                     mVelocity = mMaximumMajorVelocity;  
  27.                     mAnimatedAcceleration = mMaximumAcceleration;  
  28.                 }  
  29.                 else  
  30.                 {  
  31.                     mVelocity = -mMaximumMajorVelocity;  
  32.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  33.   
  34.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  35.                     {  
  36.                         mTrackX = mContentWidth;  
  37.                     }  
  38.                 }  
  39.                 break;  
  40.             case TOP:  
  41.                 if (mIsShrinking)  
  42.                 {  
  43.                     mVelocity = -mMaximumMajorVelocity;  
  44.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  45.                 }  
  46.                 else  
  47.                 {  
  48.                     mVelocity = mMaximumMajorVelocity;  
  49.                     mAnimatedAcceleration = mMaximumAcceleration;  
  50.   
  51.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  52.                     {  
  53.                         mTrackY = -mContentHeight;  
  54.                     }  
  55.                 }  
  56.                 break;  
  57.             case BOTTOM:  
  58.                 if (mIsShrinking)  
  59.                 {  
  60.                     mVelocity = mMaximumMajorVelocity;  
  61.                     mAnimatedAcceleration = mMaximumAcceleration;  
  62.                 }  
  63.                 else  
  64.                 {  
  65.                     mVelocity = -mMaximumMajorVelocity;  
  66.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  67.   
  68.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  69.                     {  
  70.                         mTrackY = mContentHeight;  
  71.                     }  
  72.                 }  
  73.                 break;  
  74.         }  
  75.   
  76.         if (mState == State.TRACKING)  
  77.         {  
  78.             if (mIsShrinking)  
  79.             {  
  80.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) < mContentHeight / 2)  
  81.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) < mContentWidth <span style="white-space:pre">                     </span>/ 2))  
  82.                 {  
  83.                     mVelocity = -mVelocity;  
  84.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  85.                     mIsShrinking = !mIsShrinking;  
  86.                 }  
  87.             }  
  88.             else  
  89.             {  
  90.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) > mContentHeight / 2)  
  91.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) > mContentWidth <span style="white-space:pre">                     </span>/ 2))  
  92.                 {  
  93.                     mVelocity = -mVelocity;  
  94.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  95.                     mIsShrinking = !mIsShrinking;  
  96.                 }  
  97.             }  
  98.         }  
  99.         if (mState != State.FLYING && mState != State.TRACKING)  
  100.         {  
  101.             mState = State.CLICK;  
  102.         }  
  103.     }  


其中类似

[java] view plaincopy
  1. if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  2.             {  
  3.                 mTrackX = -mContentWidth;  
  4.             }  

代码是为解决初次使用控件初始化mTrackX,否则此时单击handle会导致控件直接抽出,无动画效果


 

[java] view plaincopy
  1. if (mState == State.TRACKING)  
  2.         {  
  3.             if (mIsShrinking)  
  4.             {  
  5.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) < mContentHeight / 2)  
  6.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) < mContentWidth <span style="white-space:pre">                     </span>/ 2))  
  7.                 {  
  8.                     mVelocity = -mVelocity;  
  9.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  10.                     mIsShrinking = !mIsShrinking;  
  11.                 }  
  12.             }  
  13.             else  
  14.             {  
  15.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) > mContentHeight / 2)  
  16.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) > mContentWidth <span style="white-space:pre">                     </span>/ 2))  
  17.                 {  
  18.                     mVelocity = -mVelocity;  
  19.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  20.                     mIsShrinking = !mIsShrinking;  
  21.                 }  
  22.             }  
  23.         }  


这段代码是判断抽屉拉出是否过半,也就是决定控件在松开鼠标时是回到关闭状态还是抽出状态

 

doAnimation()方法比较简单,没啥可讲的


之前的变量 mDuration mLinearFlying mInterpolator 没用到,因为感觉没啥用,目前的控件已能满足大部分人需求,有特殊需求的请自行添加


下面上完整代码,不懂的可以留言问我

初次写博文,不周之处,还请见谅


Panel.java

 

[java] view plaincopy
  1. package org.miscwidgets.widget;  
  2.   
  3. import org.miscwidgets.R;  
  4.   
  5. import android.content.Context;  
  6. import android.content.res.TypedArray;  
  7. import android.graphics.Canvas;  
  8. import android.graphics.drawable.Drawable;  
  9. import android.os.Handler;  
  10. import android.os.Message;  
  11. import android.os.SystemClock;  
  12. import android.util.AttributeSet;  
  13. import android.util.Log;  
  14. import android.view.GestureDetector;  
  15. import android.view.MotionEvent;  
  16. import android.view.View;  
  17. import android.view.ViewGroup;  
  18. import android.view.ViewParent;  
  19. import android.view.GestureDetector.OnGestureListener;  
  20. import android.view.animation.Interpolator;  
  21. import android.widget.FrameLayout;  
  22. import android.widget.LinearLayout;  
  23. /** 
  24.  *  
  25.  * Fixed by http://blog.csdn.net/lovehong0306/article/details/7451264 
  26.  *  
  27.  */  
  28. public class Panel extends LinearLayout  
  29. {  
  30.   
  31.     private static final String TAG = "Panel";  
  32.   
  33.     private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;  
  34.     private static final float MAXIMUM_ACCELERATION = 2000.0f;  
  35.     private static final int MSG_ANIMATE = 1000;  
  36.     private static final int MSG_PREPARE_ANIMATE = 2000;  
  37.     private static final int ANIMATION_FRAME_DURATION = 1000 / 60;  
  38.   
  39.     private final Handler mHandler = new SlidingHandler();  
  40.     private float mAnimatedAcceleration;  
  41.     private long mAnimationLastTime;  
  42.     private long mCurrentAnimationTime;  
  43.     private boolean mAnimating;  
  44.   
  45.     private final int mMaximumMajorVelocity;  
  46.     private final int mMaximumAcceleration;  
  47.   
  48.     private float lastRawX, lastRawY, curRawX, curRawY;  
  49.     private float lastEventTime, curEventTime;  
  50.   
  51.     /** 
  52.      * Callback invoked when the panel is opened/closed. 
  53.      */  
  54.     public static interface OnPanelListener  
  55.     {  
  56.         /** 
  57.          * Invoked when the panel becomes fully closed. 
  58.          */  
  59.         public void onPanelClosed(Panel panel);  
  60.   
  61.         /** 
  62.          * Invoked when the panel becomes fully opened. 
  63.          */  
  64.         public void onPanelOpened(Panel panel);  
  65.     }  
  66.   
  67.     private boolean mIsShrinking;  
  68.     private int mPosition;  
  69.     private int mDuration;  
  70.     private boolean mLinearFlying;  
  71.     private int mHandleId;  
  72.     private int mContentId;  
  73.     private View mHandle;  
  74.     private View mContent;  
  75.     private Drawable mOpenedHandle;  
  76.     private Drawable mClosedHandle;  
  77.     private float mTrackX;  
  78.     private float mTrackY;  
  79.     private float mVelocity;  
  80.   
  81.     private OnPanelListener panelListener;  
  82.   
  83.     public static final int TOP = 0;  
  84.     public static final int BOTTOM = 1;  
  85.     public static final int LEFT = 2;  
  86.     public static final int RIGHT = 3;  
  87.   
  88.     private enum State  
  89.     {  
  90.         ABOUT_TO_ANIMATE, ANIMATING, READY, TRACKING, FLYING, CLICK  
  91.     };  
  92.   
  93.     private State mState;  
  94.     private Interpolator mInterpolator;  
  95.     private GestureDetector mGestureDetector;  
  96.     private int mContentHeight;  
  97.     private int mContentWidth;  
  98.     private int mOrientation;  
  99.     private float mWeight;  
  100.     private PanelOnGestureListener mGestureListener;  
  101.     private boolean mBringToFront;  
  102.   
  103.     public Panel(Context context, AttributeSet attrs)  
  104.     {  
  105.         super(context, attrs);  
  106.         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Panel);  
  107.         mDuration = a.getInteger(R.styleable.Panel_animationDuration, 750);     // duration defaults to 750 ms  
  108.         mPosition = a.getInteger(R.styleable.Panel_position, BOTTOM);           // position defaults to BOTTOM  
  109.         mLinearFlying = a.getBoolean(R.styleable.Panel_linearFlying, false);    // linearFlying defaults to false  
  110.         mWeight = a.getFraction(R.styleable.Panel_weight, 010.0f);          // weight defaults to 0.0  
  111.         if (mWeight < 0 || mWeight > 1)  
  112.         {  
  113.             mWeight = 0.0f;  
  114.             Log.w(TAG, a.getPositionDescription()  
  115.                     + ": weight must be > 0 and <= 1");  
  116.         }  
  117.         mOpenedHandle = a.getDrawable(R.styleable.Panel_openedHandle);  
  118.         mClosedHandle = a.getDrawable(R.styleable.Panel_closedHandle);  
  119.   
  120.         RuntimeException e = null;  
  121.         mHandleId = a.getResourceId(R.styleable.Panel_handle, 0);  
  122.         if (mHandleId == 0)  
  123.         {  
  124.             e = new IllegalArgumentException(  
  125.                     a.getPositionDescription()  
  126.                             + ": The handle attribute is required and must refer to a valid child.");  
  127.         }  
  128.         mContentId = a.getResourceId(R.styleable.Panel_content, 0);  
  129.         if (mContentId == 0)  
  130.         {  
  131.             e = new IllegalArgumentException(  
  132.                     a.getPositionDescription()  
  133.                             + ": The content attribute is required and must refer to a valid child.");  
  134.         }  
  135.         a.recycle();  
  136.   
  137.         final float density = getResources().getDisplayMetrics().density;  
  138.         mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);  
  139.         mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);  
  140.   
  141.         if (e != null)  
  142.         {  
  143.             throw e;  
  144.         }  
  145.         mOrientation = (mPosition == TOP || mPosition == BOTTOM) ? VERTICAL  
  146.                 : HORIZONTAL;  
  147.         setOrientation(mOrientation);  
  148.         mState = State.READY;  
  149.         mGestureListener = new PanelOnGestureListener();  
  150.         mGestureDetector = new GestureDetector(mGestureListener);  
  151.         mGestureDetector.setIsLongpressEnabled(false);  
  152.   
  153.         // i DON'T really know why i need this...  
  154.         setBaselineAligned(false);  
  155.     }  
  156.   
  157.     /** 
  158.      * Sets the listener that receives a notification when the panel becomes 
  159.      * open/close. 
  160.      *  
  161.      * @param onPanelListener 
  162.      *            The listener to be notified when the panel is opened/closed. 
  163.      */  
  164.     public void setOnPanelListener(OnPanelListener onPanelListener)  
  165.     {  
  166.         panelListener = onPanelListener;  
  167.     }  
  168.   
  169.     /** 
  170.      * Gets Panel's mHandle 
  171.      *  
  172.      * @return Panel's mHandle 
  173.      */  
  174.     public View getHandle()  
  175.     {  
  176.         return mHandle;  
  177.     }  
  178.   
  179.     /** 
  180.      * Gets Panel's mContent 
  181.      *  
  182.      * @return Panel's mContent 
  183.      */  
  184.     public View getContent()  
  185.     {  
  186.         return mContent;  
  187.     }  
  188.   
  189.     /** 
  190.      * Sets the acceleration curve for panel's animation. 
  191.      *  
  192.      * @param i 
  193.      *            The interpolator which defines the acceleration curve 
  194.      */  
  195.     public void setInterpolator(Interpolator i)  
  196.     {  
  197.         mInterpolator = i;  
  198.     }  
  199.   
  200.     /** 
  201.      * Set the opened state of Panel. 
  202.      *  
  203.      * @param open 
  204.      *            True if Panel is to be opened, false if Panel is to be closed. 
  205.      * @param animate 
  206.      *            True if use animation, false otherwise. 
  207.      *  
  208.      * @return True if operation was performed, false otherwise. 
  209.      *  
  210.      */  
  211.     public boolean setOpen(boolean open, boolean animate)  
  212.     {  
  213.         if (mState == State.READY && isOpen() ^ open)  
  214.         {  
  215.             mIsShrinking = !open;  
  216.             if (animate)  
  217.             {  
  218.                 mState = State.ABOUT_TO_ANIMATE;  
  219.                 if (!mIsShrinking)  
  220.                 {  
  221.                     // this could make flicker so we test mState in  
  222.                     // dispatchDraw()  
  223.                     // to see if is equal to ABOUT_TO_ANIMATE  
  224.                     mContent.setVisibility(VISIBLE);  
  225.                 }  
  226.                 long now = SystemClock.uptimeMillis();  
  227.                 mAnimationLastTime = now;  
  228.                 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;  
  229.                 mAnimating = true;  
  230.                 mHandler.removeMessages(MSG_ANIMATE);  
  231.                 mHandler.removeMessages(MSG_PREPARE_ANIMATE);  
  232.                 mHandler.sendMessageAtTime(  
  233.                         mHandler.obtainMessage(MSG_PREPARE_ANIMATE),  
  234.                         mCurrentAnimationTime);  
  235.             }  
  236.             else  
  237.             {  
  238.                 mContent.setVisibility(open ? VISIBLE : GONE);  
  239.                 postProcess();  
  240.             }  
  241.             return true;  
  242.         }  
  243.         return false;  
  244.     }  
  245.   
  246.     /** 
  247.      * Returns the opened status for Panel. 
  248.      *  
  249.      * @return True if Panel is opened, false otherwise. 
  250.      */  
  251.     public boolean isOpen()  
  252.     {  
  253.         return mContent.getVisibility() == VISIBLE;  
  254.     }  
  255.   
  256.     @Override  
  257.     protected void onFinishInflate()  
  258.     {  
  259.         super.onFinishInflate();  
  260.         mHandle = findViewById(mHandleId);  
  261.         if (mHandle == null)  
  262.         {  
  263.             String name = getResources().getResourceEntryName(mHandleId);  
  264.             throw new RuntimeException(  
  265.                     "Your Panel must have a child View whose id attribute is 'R.id."  
  266.                             + name + "'");  
  267.         }  
  268.         mHandle.setClickable(true);  
  269.         mHandle.setOnTouchListener(touchListener);  
  270.         // mHandle.setOnClickListener(clickListener);  
  271.   
  272.         mContent = findViewById(mContentId);  
  273.         if (mContent == null)  
  274.         {  
  275.             String name = getResources().getResourceEntryName(mHandleId);  
  276.             throw new RuntimeException(  
  277.                     "Your Panel must have a child View whose id attribute is 'R.id."  
  278.                             + name + "'");  
  279.         }  
  280.   
  281.         // reposition children  
  282.         removeView(mHandle);  
  283.         removeView(mContent);  
  284.         if (mPosition == TOP || mPosition == LEFT)  
  285.         {  
  286.             addView(mContent);  
  287.             addView(mHandle);  
  288.         }  
  289.         else  
  290.         {  
  291.             addView(mHandle);  
  292.             addView(mContent);  
  293.         }  
  294.   
  295.         if (mClosedHandle != null)  
  296.         {  
  297.             mHandle.setBackgroundDrawable(mClosedHandle);  
  298.         }  
  299.         mContent.setClickable(true);  
  300.         mContent.setVisibility(GONE);  
  301.         if (mWeight > 0)  
  302.         {  
  303.             ViewGroup.LayoutParams params = mContent.getLayoutParams();  
  304.             if (mOrientation == VERTICAL)  
  305.             {  
  306.                 params.height = ViewGroup.LayoutParams.FILL_PARENT;  
  307.             }  
  308.             else  
  309.             {  
  310.                 params.width = ViewGroup.LayoutParams.FILL_PARENT;  
  311.             }  
  312.             mContent.setLayoutParams(params);  
  313.         }  
  314.     }  
  315.   
  316.     @Override  
  317.     protected void onAttachedToWindow()  
  318.     {  
  319.         super.onAttachedToWindow();  
  320.         ViewParent parent = getParent();  
  321.         if (parent != null && parent instanceof FrameLayout)  
  322.         {  
  323.             mBringToFront = true;  
  324.         }  
  325.     }  
  326.   
  327.     @Override  
  328.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
  329.     {  
  330.         if (mWeight > 0 && mContent.getVisibility() == VISIBLE)  
  331.         {  
  332.             View parent = (View) getParent();  
  333.             if (parent != null)  
  334.             {  
  335.                 if (mOrientation == VERTICAL)  
  336.                 {  
  337.                     heightMeasureSpec = MeasureSpec.makeMeasureSpec(  
  338.                             (int) (parent.getHeight() * mWeight),  
  339.                             MeasureSpec.EXACTLY);  
  340.                 }  
  341.                 else  
  342.                 {  
  343.                     widthMeasureSpec = MeasureSpec.makeMeasureSpec(  
  344.                             (int) (parent.getWidth() * mWeight),  
  345.                             MeasureSpec.EXACTLY);  
  346.                 }  
  347.             }  
  348.         }  
  349.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  350.     }  
  351.   
  352.     @Override  
  353.     protected void onLayout(boolean changed, int l, int t, int r, int b)  
  354.     {  
  355.         super.onLayout(changed, l, t, r, b);  
  356.         mContentWidth = mContent.getWidth();  
  357.         mContentHeight = mContent.getHeight();  
  358.     }  
  359.   
  360.     @Override  
  361.     protected void dispatchDraw(Canvas canvas)  
  362.     {  
  363.         // String name = getResources().getResourceEntryName(getId());  
  364.         // Log.d(TAG, name + " ispatchDraw " + mState);  
  365.         // this is why 'mState' was added:  
  366.         // avoid flicker before animation start  
  367.         if (mState == State.ABOUT_TO_ANIMATE && !mIsShrinking)  
  368.         {  
  369.             int delta = mOrientation == VERTICAL ? mContentHeight  
  370.                     : mContentWidth;  
  371.             if (mPosition == LEFT || mPosition == TOP)  
  372.             {  
  373.                 delta = -delta;  
  374.             }  
  375.             if (mOrientation == VERTICAL)  
  376.             {  
  377.                 canvas.translate(0, delta);  
  378.             }  
  379.             else  
  380.             {  
  381.                 canvas.translate(delta, 0);  
  382.             }  
  383.         }  
  384.         if (mState == State.TRACKING || mState == State.FLYING  
  385.                 || mState == State.CLICK)  
  386.         {  
  387.             canvas.translate(mTrackX, mTrackY);  
  388.         }  
  389.         super.dispatchDraw(canvas);  
  390.     }  
  391.   
  392.     private float ensureRange(float v, int min, int max)  
  393.     {  
  394.         v = Math.max(v, min);  
  395.         v = Math.min(v, max);  
  396.         return v;  
  397.     }  
  398.   
  399.     OnTouchListener touchListener = new OnTouchListener()  
  400.     {  
  401.           
  402.         public boolean onTouch(View v, MotionEvent event)  
  403.         {  
  404.   
  405.             if (mAnimating)  
  406.             {  
  407.                 // we are animating  
  408.                 return true;// 动画中不响应onTouch事件  
  409.             }  
  410.   
  411.             int action = event.getAction();  
  412.             if (action == MotionEvent.ACTION_DOWN)  
  413.             {  
  414.                 if (mBringToFront)  
  415.                 {  
  416.                     bringToFront();  
  417.                 }  
  418.             }  
  419.   
  420.             if (!mGestureDetector.onTouchEvent(event))  
  421.             {  
  422.                 if (action == MotionEvent.ACTION_UP)  
  423.                 {  
  424.                     // tup up after scrolling  
  425.   
  426.                     long now = SystemClock.uptimeMillis();  
  427.                     mAnimationLastTime = now;  
  428.                     mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;  
  429.                     mAnimating = true;  
  430.                     mHandler.removeMessages(MSG_ANIMATE);  
  431.                     mHandler.removeMessages(MSG_PREPARE_ANIMATE);  
  432.                     mHandler.sendMessageAtTime(  
  433.                             mHandler.obtainMessage(MSG_PREPARE_ANIMATE),  
  434.                             mCurrentAnimationTime);  
  435.                 }  
  436.             }  
  437.             return false;  
  438.         }  
  439.     };  
  440.   
  441.     public boolean initChange()  
  442.     {  
  443.         if (mState != State.READY)  
  444.         {  
  445.             // we are animating or just about to animate  
  446.             return false;  
  447.         }  
  448.         mState = State.ABOUT_TO_ANIMATE;  
  449.         mIsShrinking = mContent.getVisibility() == VISIBLE;  
  450.         if (!mIsShrinking)  
  451.         {  
  452.             // this could make flicker so we test mState in dispatchDraw()  
  453.             // to see if is equal to ABOUT_TO_ANIMATE  
  454.             mContent.setVisibility(VISIBLE);  
  455.         }  
  456.         return true;  
  457.     }  
  458.   
  459.     private void postProcess()  
  460.     {  
  461.         if (mIsShrinking && mClosedHandle != null)  
  462.         {  
  463.             mHandle.setBackgroundDrawable(mClosedHandle);  
  464.         }  
  465.         else if (!mIsShrinking && mOpenedHandle != null)  
  466.         {  
  467.             mHandle.setBackgroundDrawable(mOpenedHandle);  
  468.         }  
  469.         // invoke listener if any  
  470.         if (panelListener != null)  
  471.         {  
  472.             if (mIsShrinking)  
  473.             {  
  474.                 panelListener.onPanelClosed(Panel.this);  
  475.             }  
  476.             else  
  477.             {  
  478.                 panelListener.onPanelOpened(Panel.this);  
  479.             }  
  480.         }  
  481.     }  
  482.   
  483.     class PanelOnGestureListener implements OnGestureListener  
  484.     {  
  485.         float scrollY;  
  486.         float scrollX;  
  487.   
  488.         @Override  
  489.         public boolean onDown(MotionEvent e)  
  490.         {  
  491.             scrollX = scrollY = 0;  
  492.             lastRawX = curRawX = lastRawY = curRawY = -1;  
  493.             lastEventTime = curEventTime = -1;  
  494.             initChange();  
  495.             return true;  
  496.         }  
  497.   
  498.         @Override  
  499.         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,  
  500.                 float velocityY)  
  501.         {  
  502.             mState = State.FLYING;  
  503.   
  504.             float velocityX2, velocityY2;  
  505.             if (lastRawX == -1 && lastRawY == -1)   //见onScroll方法  
  506.             {  
  507.                 velocityX2 = (curRawX - e1.getRawX())  
  508.                         / (curEventTime - e1.getEventTime()) * 1000//  px/s  
  509.                 velocityY2 = (curRawY - e1.getRawY())  
  510.                         / (curEventTime - e1.getEventTime()) * 1000;  
  511.             }  
  512.             else  
  513.             {  
  514.                 velocityX2 = (curRawX - lastRawX)  
  515.                         / (curEventTime - lastEventTime) * 1000;  
  516.                 velocityY2 = (curRawY - lastRawY)  
  517.                         / (curEventTime - lastEventTime) * 1000;  
  518.             }  
  519.   
  520.             mVelocity = mOrientation == VERTICAL ? velocityY2 : velocityX2;  
  521.   
  522.             if (Math.abs(mVelocity) > 50)  
  523.             {  
  524.                 if (mVelocity > 0)  
  525.                 {  
  526.                     mAnimatedAcceleration = mMaximumAcceleration;  
  527.                 }  
  528.                 else  
  529.                 {  
  530.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  531.                 }  
  532.   
  533.                 long now = SystemClock.uptimeMillis();  
  534.                 mAnimationLastTime = now;  
  535.                 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;  
  536.                 mAnimating = true;  
  537.                 mHandler.removeMessages(MSG_ANIMATE);  
  538.                 mHandler.removeMessages(MSG_PREPARE_ANIMATE);  
  539.                 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),  
  540.                         mCurrentAnimationTime);  
  541.                 return true;  
  542.             }  
  543.             return false;  
  544.         }  
  545.   
  546.         @Override  
  547.         public void onLongPress(MotionEvent e)  
  548.         {  
  549.             // not used  
  550.         }  
  551.   
  552.         @Override  
  553.         public boolean onScroll(MotionEvent e1, MotionEvent e2,  
  554.                 float distanceX, float distanceY)  
  555.         {  
  556.             mState = State.TRACKING;  
  557.             float tmpY = 0, tmpX = 0;  
  558.             if (mOrientation == VERTICAL)  
  559.             {  
  560.                 scrollY -= distanceY;  
  561.                 if (mPosition == TOP)  
  562.                 {  
  563.                     tmpY = ensureRange(scrollY, -mContentHeight, 0);  
  564.                 }  
  565.                 else  
  566.                 {  
  567.                     tmpY = ensureRange(scrollY, 0, mContentHeight);  
  568.                 }  
  569.             }  
  570.             else  
  571.             {  
  572.                 scrollX -= distanceX;  
  573.                 if (mPosition == LEFT)  
  574.                 {  
  575.                     tmpX = ensureRange(scrollX, -mContentWidth, 0);  
  576.                 }  
  577.                 else  
  578.                 {  
  579.                     tmpX = ensureRange(scrollX, 0, mContentWidth);  
  580.                 }  
  581.             }  
  582.   
  583.             if (tmpX != mTrackX || tmpY != mTrackY)  
  584.             {  
  585.                 mTrackX = tmpX;  
  586.                 mTrackY = tmpY;  
  587.                 // invalidate(); //放在此导致极快速滑动至touch区域外界面不刷新(mTrackX、mTrackY均为0)  
  588.             }  
  589.             invalidate();  
  590.   
  591.             lastRawX = curRawX;  
  592.             lastRawY = curRawY;  
  593.             lastEventTime = curEventTime;  
  594.             curRawX = e2.getRawX();  
  595.             curRawY = e2.getRawY();  
  596.             curEventTime = e2.getEventTime();  
  597.             return true;  
  598.         }  
  599.   
  600.         @Override  
  601.         public void onShowPress(MotionEvent e)  
  602.         {  
  603.             // not used  
  604.         }  
  605.   
  606.         @Override  
  607.         public boolean onSingleTapUp(MotionEvent e)  
  608.         {  
  609.             // not used  
  610.             return false;  
  611.         }  
  612.     }  
  613.   
  614.     private void prepareAnimation()  
  615.     {  
  616.   
  617.         switch (mPosition)  
  618.         {  
  619.             case LEFT:  
  620.                 if (mIsShrinking)  
  621.                 {  
  622.                     mVelocity = -mMaximumMajorVelocity;  
  623.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  624.   
  625.                 }  
  626.                 else  
  627.                 {  
  628.                     mVelocity = mMaximumMajorVelocity;  
  629.                     mAnimatedAcceleration = mMaximumAcceleration;  
  630.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  631.                     {  
  632.                         mTrackX = -mContentWidth;  
  633.                     }  
  634.                 }  
  635.                 break;  
  636.             case RIGHT:  
  637.                 if (mIsShrinking)  
  638.                 {  
  639.                     mVelocity = mMaximumMajorVelocity;  
  640.                     mAnimatedAcceleration = mMaximumAcceleration;  
  641.                 }  
  642.                 else  
  643.                 {  
  644.                     mVelocity = -mMaximumMajorVelocity;  
  645.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  646.   
  647.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  648.                     {  
  649.                         mTrackX = mContentWidth;  
  650.                     }  
  651.                 }  
  652.                 break;  
  653.             case TOP:  
  654.                 if (mIsShrinking)  
  655.                 {  
  656.                     mVelocity = -mMaximumMajorVelocity;  
  657.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  658.                 }  
  659.                 else  
  660.                 {  
  661.                     mVelocity = mMaximumMajorVelocity;  
  662.                     mAnimatedAcceleration = mMaximumAcceleration;  
  663.   
  664.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  665.                     {  
  666.                         mTrackY = -mContentHeight;  
  667.                     }  
  668.                 }  
  669.                 break;  
  670.             case BOTTOM:  
  671.                 if (mIsShrinking)  
  672.                 {  
  673.                     mVelocity = mMaximumMajorVelocity;  
  674.                     mAnimatedAcceleration = mMaximumAcceleration;  
  675.                 }  
  676.                 else  
  677.                 {  
  678.                     mVelocity = -mMaximumMajorVelocity;  
  679.                     mAnimatedAcceleration = -mMaximumAcceleration;  
  680.   
  681.                     if (mTrackX == 0 && mState == State.ABOUT_TO_ANIMATE)  
  682.                     {  
  683.                         mTrackY = mContentHeight;  
  684.                     }  
  685.                 }  
  686.                 break;  
  687.         }  
  688.   
  689.         if (mState == State.TRACKING)  
  690.         {  
  691.             if (mIsShrinking)  
  692.             {  
  693.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) < mContentHeight / 2)  
  694.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) < mContentWidth / 2))  
  695.                 {  
  696.                     mVelocity = -mVelocity;  
  697.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  698.                     mIsShrinking = !mIsShrinking;  
  699.                 }  
  700.             }  
  701.             else  
  702.             {  
  703.                 if ((mOrientation == VERTICAL && Math.abs(mTrackY) > mContentHeight / 2)  
  704.                         || (mOrientation == HORIZONTAL && Math.abs(mTrackX) > mContentWidth / 2))  
  705.                 {  
  706.                     mVelocity = -mVelocity;  
  707.                     mAnimatedAcceleration = -mAnimatedAcceleration;  
  708.                     mIsShrinking = !mIsShrinking;  
  709.                 }  
  710.             }  
  711.         }  
  712.         if (mState != State.FLYING && mState != State.TRACKING)  
  713.         {  
  714.             mState = State.CLICK;  
  715.         }  
  716.     }  
  717.   
  718.     private void doAnimation()  
  719.     {  
  720.   
  721.         if (mAnimating)  
  722.         {  
  723.             long now = SystemClock.uptimeMillis();  
  724.             float t = (now - mAnimationLastTime) / 1000.0f;     // ms -> s  
  725.             final float v = mVelocity;                          // px/s  
  726.             final float a = mAnimatedAcceleration;              // px/s/s  
  727.             mVelocity = v + (a * t);                            // px/s  
  728.             mAnimationLastTime = now;  
  729.   
  730.             switch (mPosition)  
  731.             {  
  732.                 case LEFT:  
  733.                     mTrackX = mTrackX + (v * t) + (0.5f * a * t * t); // px  
  734.                     if (mTrackX > 0)  
  735.                     {  
  736.                         mTrackX = 0;  
  737.                         mState = State.READY;  
  738.                         mAnimating = false;  
  739.                     }  
  740.                     else if (mTrackX < -mContentWidth)  
  741.                     {  
  742.                         mTrackX = -mContentWidth;  
  743.                         mContent.setVisibility(GONE);  
  744.                         mState = State.READY;  
  745.                         mAnimating = false;  
  746.                     }  
  747.                     break;  
  748.                 case RIGHT:  
  749.                     mTrackX = mTrackX + (v * t) + (0.5f * a * t * t);  
  750.                     if (mTrackX < 0)  
  751.                     {  
  752.                         mTrackX = 0;  
  753.                         mState = State.READY;  
  754.                         mAnimating = false;  
  755.                     }  
  756.                     else if (mTrackX > mContentWidth)  
  757.                     {  
  758.                         mTrackX = mContentWidth;  
  759.                         mContent.setVisibility(GONE);  
  760.                         mState = State.READY;  
  761.                         mAnimating = false;  
  762.                     }  
  763.                     break;  
  764.                 case TOP:  
  765.                     mTrackY = mTrackY + (v * t) + (0.5f * a * t * t);  
  766.                     if (mTrackY > 0)  
  767.                     {  
  768.                         mTrackY = 0;  
  769.                         mState = State.READY;  
  770.                         mAnimating = false;  
  771.                     }  
  772.                     else if (mTrackY < -mContentHeight)  
  773.                     {  
  774.                         mTrackY = -mContentHeight;  
  775.                         mContent.setVisibility(GONE);  
  776.                         mState = State.READY;  
  777.                         mAnimating = false;  
  778.                     }  
  779.                     break;  
  780.                 case BOTTOM:  
  781.                     mTrackY = mTrackY + (v * t) + (0.5f * a * t * t);  
  782.                     if (mTrackY < 0)  
  783.                     {  
  784.                         mTrackY = 0;  
  785.                         mState = State.READY;  
  786.                         mAnimating = false;  
  787.                     }  
  788.                     else if (mTrackY > mContentHeight)  
  789.                     {  
  790.                         mTrackY = mContentHeight;  
  791.                         mContent.setVisibility(GONE);  
  792.                         mState = State.READY;  
  793.                         mAnimating = false;  
  794.                     }  
  795.                     break;  
  796.             }  
  797.             invalidate();  
  798.   
  799.             if (!mAnimating)  
  800.             {  
  801.                 postProcess();  
  802.                 return;  
  803.             }  
  804.             mCurrentAnimationTime += ANIMATION_FRAME_DURATION;  
  805.             mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),  
  806.                     mCurrentAnimationTime);  
  807.   
  808.         }  
  809.     }  
  810.   
  811.     private class SlidingHandler extends Handler  
  812.     {  
  813.         public void handleMessage(Message m)  
  814.         {  
  815.             switch (m.what)  
  816.             {  
  817.                 case MSG_ANIMATE:  
  818.                     doAnimation();  
  819.                     break;  
  820.                 case MSG_PREPARE_ANIMATE:  
  821.                     prepareAnimation();  
  822.                     doAnimation();  
  823.                     break;  
  824.             }  
  825.         }  
  826.     }  
  827. }  


工程下载地址:

http://download.csdn.net/detail/lovehong0306/4230052

0
2
分享到:
评论

相关推荐

    android-misc-widgets

    "Android Misc Widgets" 是一个专为Android平台设计的开源项目,其主要目的是提供一系列具有特殊效果和动画的组件,供开发者学习和参考。这个项目的源码涵盖了多种Android小部件的实现,是Android开发者提升技能、...

    android-misc-widgets.rar

    "android-misc-widgets.rar"这个压缩包中,很可能包含了多种不同类型的Android小部件示例,用于展示和学习如何在实际项目中创建和定制各种小部件。下面我们将详细探讨Android小部件的相关知识点。 1. **小部件基础...

    AndroidMiscWidgets多方抽屉

    总的来说,"AndroidMiscWidgets多方抽屉"是一个致力于提高Android应用界面交互性的库,它解决了抽屉组件可能出现的闪烁问题,提供了多样化的抽屉方向选择,同时优化了左侧抽屉体验。对于Android开发者来说,这个库...

    Python库 | scikit-misc-0.1.1.tar.gz

    **Python库scikit-misc-0.1.1.tar.gz详解** 在Python的世界里,库是程序员们构建复杂应用的基础,它们提供了丰富的功能,减少了重复劳动。`scikit-misc`是一个这样的库,专为Python开发人员设计,旨在提供一些在...

    lucene-misc-6.6.0-API文档-中文版.zip

    包含翻译后的API文档:lucene-misc-6.6.0-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.apache.lucene:lucene-misc:6.6.0; 标签:apache、lucene、misc、jar包、java、中文文档; 使用方法:解压翻译后的API...

    xorg-x11-fonts-misc-7.5-33.el9.noarch

    xorg-x11-fonts-misc-7.5-33.el9.noarch

    lucene-misc-2.1.0.jar下载

    lucene-misc-2.1.0.jar lucene-misc-2.1.0.jar

    lucene-misc-6.6.0-API文档-中英对照版.zip

    赠送jar包:lucene-misc-6.6.0.jar; 赠送原API文档:lucene-misc-6.6.0-javadoc.jar; 赠送源代码:lucene-misc-6.6.0-sources.jar; 赠送Maven依赖信息文件:lucene-misc-6.6.0.pom; 包含翻译后的API文档:lucene...

    lucene-misc-7.7.0-API文档-中文版.zip

    包含翻译后的API文档:lucene-misc-7.7.0-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.7.0; 标签:apache、lucene、misc、中文文档、jar包、java; 使用方法:解压翻译后的API...

    lucene-misc-7.2.1-API文档-中文版.zip

    包含翻译后的API文档:lucene-misc-7.2.1-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.2.1; 标签:apache、lucene、misc、中文文档、jar包、java; 使用方法:解压翻译后的API...

    lucene-misc-7.3.1-API文档-中文版.zip

    包含翻译后的API文档:lucene-misc-7.3.1-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.3.1; 标签:apache、lucene、misc、中文文档、jar包、java; 使用方法:解压翻译后的API...

    Android提高之多方向抽屉实现方法

    android-misc-widgets里面包含几个widget:Panel、SmoothButton、Switcher、VirtualKeyboard,还有一些动画特效,本文主要介绍抽屉容器Panel的用法。android-misc-widgets的google工程地址:-widgets/...

    xorg-x11-fonts-misc-7.2-11.el6.noarch.rpm

    xorg-x11-fonts-misc-7.2-11.el6.noarch

    xorg-x11-fonts-misc-7.2-9.1.el6.noarch.rpm

    安装vnc-server依赖包

    lucene-misc-7.7.0-API文档-中英对照版.zip

    包含翻译后的API文档:lucene-misc-7.7.0-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.7.0; 标签:apache、lucene、misc、中英对照文档、jar包、java; 使用方法:...

    lucene-misc-7.2.1-API文档-中英对照版.zip

    包含翻译后的API文档:lucene-misc-7.2.1-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.2.1; 标签:apache、lucene、misc、中英对照文档、jar包、java; 使用方法:...

    lucene-misc-7.3.1-API文档-中英对照版.zip

    包含翻译后的API文档:lucene-misc-7.3.1-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.apache.lucene:lucene-misc:7.3.1; 标签:apache、lucene、misc、中英对照文档、jar包、java; 使用方法:...

    ISCC2021-MISC部分题目.zip

    ISCC2021-MISC部分题目.zip是一个与ISCC(International Symposium on Computer Science and Convergence,国际计算机科学与融合大会)相关的压缩包文件。这个压缩包可能包含了该会议的多份论文、研究报告或者竞赛...

    jira-misc-workflow-extensions-2.5.7.1.jar jira 流扩展插件

    1、jira-misc-workflow-extensions-2.5.7.1.jar; 2、jira流扩展插件; 3、提供更多的 触发条件/校验条件/结果处理;

Global site tag (gtag.js) - Google Analytics