美文网首页
android 点击button状态变化实现分析

android 点击button状态变化实现分析

作者: JeremyDai | 来源:发表于2016-07-01 00:22 被阅读1698次

    前言

    在写应用时 ,布局中设置了一个button之后点击这个button,button的背景就会变化,我们什么都没干就有了这个效果,最近项目正好涉及到相关的绘制,就去源码里面看一下点击时的实现。关于点击事件的传递我在的android 中事件传递分析 已经学习过了,其中还涉及到view的绘制view绘制流程

    内容

    点击某个button时,首先时ViewGroup接收到这个事件然后分发下去dispatchTouchEvent,最后到了你的button的(button继承自view)的onTouchEvent中

    
        /**
         * Implement this method to handle touch screen motion events.
         * <p>
         * If this method is used to detect click actions, it is recommended that
         * the actions be performed by implementing and calling
         * {@link #performClick()}. This will ensure consistent system behavior,
         * including:
         * <ul>
         * <li>obeying click sound preferences
         * <li>dispatching OnClickListener calls
         * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
         * accessibility features are enabled
         * </ul>
         *
         * @param event The motion event.
         * @return True if the event was handled, false otherwise.
         */
        public boolean onTouchEvent(MotionEvent event) {
            final int viewFlags = mViewFlags;
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                          if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
    //            final int action = event.getAction();
    //            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
    //                if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    //                    setPressed(false);
    //                } else if ((mPrivateFlags & PFLAG_PREPRESSED) != 0) {
    //                    removeTapCallback();
    //                }
                }
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return (((viewFlags & CLICKABLE) == CLICKABLE ||
                        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
            }
    
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_UP:
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                            // take focus if we don't have it already and we should in
                            // touch mode.
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true);
                           }
    
                            if (!mHasPerformedLongPress) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // Only perform take click actions if we were in the pressed state
                                if (!focusTaken) {
                                    // Use a Runnable and post this rather than calling
                                    // performClick directly. This lets other visual state
                                    // of the view update before click actions start.
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
                            removeTapCallback();
                        }
                        break;
    
                    case MotionEvent.ACTION_DOWN: //这个地方时你按下view的响应
                        mHasPerformedLongPress = false;
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
    
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true);// 这里设置按下为true
                            checkForLongClick(0);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        setPressed(false);
                        removeTapCallback();
                        removeLongPressCallback();
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        final int x = (int) event.getX();
                        final int y = (int) event.getY();
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            removeTapCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                // Remove any future long press/tap checks
                                removeLongPressCallback();
    
                                setPressed(false);
                            }
                        }
                        break;
                }
                return true;
            }
    
            return false;
        }
    

    setPressed(true); 那就走到了setPressed里面

    /**
         * Sets the pressed state for this view.
         *
         * @see #isClickable()
         * @see #setClickable(boolean)
         *
         * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
         *        the View's internal state from a previously set "pressed" state.
         */
        public void setPressed(boolean pressed) {
            final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
    
            if (pressed) {
                mPrivateFlags |= PFLAG_PRESSED;
            } else {
                mPrivateFlags &= ~PFLAG_PRESSED;
            }
    
            if (needsRefresh) { //是否需要刷新,当然要啦!
                refreshDrawableState();
            }
            dispatchSetPressed(pressed);
        }
    

    看看refreshDrawableState 这里是干了什么

     /**
         * Call this to force a view to update its drawable state. This will cause
         * drawableStateChanged to be called on this view. Views that are interested
         * in the new state should call getDrawableState.
         *
         * @see #drawableStateChanged
         * @see #getDrawableState
         */
        public void refreshDrawableState() {
            mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
            drawableStateChanged();
    
            ViewParent parent = mParent;
            if (parent != null) {
                parent.childDrawableStateChanged(this);//如果是个父视图的话,子view也相应更新。
            }
        }
    

    看代码的时候注意看方法上面的注释,对我们理解流程有很大的帮组,上面说一会调drawableStateChanged这个方法,我们一会看看时在那里回调的。下面看看 drawableStateChanged(),

     private Drawable mBackground;
     /**
         * This function is called whenever the state of the view changes in such
         * a way that it impacts the state of drawables being shown.
         *
         * <p>Be sure to call through to the superclass when overriding this
         * function.
         *
         * @see Drawable#setState(int[])
         */
        protected void drawableStateChanged() {
            Drawable d = mBackground;
            if (d != null && d.isStateful()) {
                d.setState(getDrawableState());
            }
        }
    

    看到了 这个地方给drawable设置了一个新的状态,通过getDrawableState()的值设置下去

     /**
         * Return an array of resource IDs of the drawable states representing the
         * current state of the view.
         *
         * @return The current drawable state
         *
         * @see Drawable#setState(int[])
         * @see #drawableStateChanged()
         * @see #onCreateDrawableState(int)
         */
        public final int[] getDrawableState() {
            if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
                return mDrawableState;
            } else {
                mDrawableState = onCreateDrawableState(0);
                mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
                return mDrawableState;
            }
        }
    

    然后看看上面d.setState()这个方法里面, drawable 是一个抽象类,看看它的setState方法,

    
        /**
         * Specify a set of states for the drawable. These are use-case specific,
         * so see the relevant documentation. As an example, the background for
         * widgets like Button understand the following states:
         * [{@link android.R.attr#state_focused},
         *  {@link android.R.attr#state_pressed}].
         *
         * <p>If the new state you are supplying causes the appearance of the
         * Drawable to change, then it is responsible for calling
         * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
         * true will be returned from this function.
         *
         * <p>Note: The Drawable holds a reference on to <var>stateSet</var>
         * until a new state array is given to it, so you must not modify this
         * array during that time.</p>
         *
         * @param stateSet The new set of states to be displayed.
         *
         * @return Returns true if this change in state has caused the appearance
         * of the Drawable to change (hence requiring an invalidate), otherwise
         * returns false.
         */
        public boolean setState(final int[] stateSet) {
            if (!Arrays.equals(mStateSet, stateSet)) {
                mStateSet = stateSet;
                return onStateChange(stateSet);
            }
            return false;
        }
    
       /**
         * Override this in your subclass to change appearance if you recognize the
         * specified state.
         *
         * @return Returns true if the state change has caused the appearance of
         * the Drawable to change (that is, it needs to be drawn), else false
         * if it looks the same and there is no need to redraw it since its
         * last state.
         */
        protected boolean onStateChange(int[] state) { return false; }
    

    Override this in your subclass ,这个方法会在drawable的子类中实现,那就要找找是哪一个子类实现了这个方法了。找了半天都没有找到哪里有指定drawable的子类,但是功夫不负苦心人,看到了setBackgroundDrawable 这个方法在view当中,

     /**
         * @deprecated use {@link #setBackground(Drawable)} instead
         */
        @Deprecated
        public void setBackgroundDrawable(Drawable background) {
            computeOpaqueFlags();
    
            if (background == mBackground) {
                return;
            }
        ....
      mBackground = background; // 你设什么类型下来,我就是怎么类型。button多个状态那肯定就是StateListDrawable了,StateListDrawable又继承自DrawableContainer,DrawableContainer继承自Drawable.
    } onStateChange
    

    那就看看StateListDrawable里面的onStateChange方法

     @Override
        protected boolean onStateChange(int[] stateSet) {
            int idx = mStateListState.indexOfStateSet(stateSet);
            if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
                    + Arrays.toString(stateSet) + " found " + idx);
            if (idx < 0) {
                idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
            }
            if (selectDrawable(idx)) {
                return true;
            }
            return super.onStateChange(stateSet);
        }
    

    看到了selectDrawable,亲人啊,selectDrawable是在它的父类DrawableContainer里面实现的,

     public boolean selectDrawable(int idx) {
            if (idx == mCurIndex) {
                return false;
            }
    
            final long now = SystemClock.uptimeMillis();
    
            if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + idx
                    + ": exit=" + mDrawableContainerState.mExitFadeDuration
                    + " enter=" + mDrawableContainerState.mEnterFadeDuration);
    
            if (mDrawableContainerState.mExitFadeDuration > 0) {
                if (mLastDrawable != null) {
                    mLastDrawable.setVisible(false, false);
                }
                if (mCurrDrawable != null) {
                    mLastDrawable = mCurrDrawable;
                    mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
                } else {
                    mLastDrawable = null;
                    mExitAnimationEnd = 0;
                }
            } else if (mCurrDrawable != null) {
                mCurrDrawable.setVisible(false, false);
            }
    
            if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
                final Drawable d = mDrawableContainerState.getChild(idx);
                mCurrDrawable = d;
                mCurIndex = idx;
                if (d != null) {
                    mInsets = d.getOpticalInsets();
                    d.mutate();
                    if (mDrawableContainerState.mEnterFadeDuration > 0) {
                        mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
                    } else {
                        d.setAlpha(mAlpha);
                    }
                    d.setVisible(isVisible(), true);
                    d.setDither(mDrawableContainerState.mDither);
                    d.setColorFilter(mColorFilter);
                    d.setState(getState());
                    d.setLevel(getLevel());
                    d.setBounds(getBounds());
                    d.setLayoutDirection(getLayoutDirection());
                    d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
                } else {
                    mInsets = Insets.NONE;
                }
            } else {
                mCurrDrawable = null;
                mInsets = Insets.NONE;
                mCurIndex = -1;
            }
    
            if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
                if (mAnimationRunnable == null) {
                    mAnimationRunnable = new Runnable() {
                        @Override public void run() {
                            animate(true);
                            invalidateSelf();
                        }
                    };
                } else {
                    unscheduleSelf(mAnimationRunnable);
                }
                // Compute first frame and schedule next animation.
                animate(true);
            }
    
            invalidateSelf();// 终于看到了invalidateSelf 方法了之前setState的注释里面有提到这个方法
    
            return true;
        }
    

    invalidateSelf方法的实现在drawable里面,这里你需要知道view是有实现Drawable的callback接口的,**public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource **

      /**
         * Use the current {@link Callback} implementation to have this Drawable
         * redrawn.  Does nothing if there is no Callback attached to the
         * Drawable.
         *
         * @see Callback#invalidateDrawable
         * @see #getCallback() 
         * @see #setCallback(android.graphics.drawable.Drawable.Callback) 
         */
        public void invalidateSelf() {
            final Callback callback = getCallback();
            if (callback != null) {
                callback.invalidateDrawable(this);//这个时候就回到了view 里面的invalidateDrawable方法里了
            }
        }
    

    View.java中

      /**
         * Invalidates the specified Drawable.
         *
         * @param drawable the drawable to invalidate
         */
        public void invalidateDrawable(Drawable drawable) {
            if (verifyDrawable(drawable)) {
                final Rect dirty = drawable.getBounds();
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
    
                invalidate(dirty.left + scrollX, dirty.top + scrollY,
                        dirty.right + scrollX, dirty.bottom + scrollY); // view 的刷新
            }
        }
    

    invalidate 的刷新是通知了逐层上传,然后最终从父类开始重绘。ok!

    invalidate 流程
    关于invalidate的刷新可以看invalidate 分析 的5-3

    相关文章

      网友评论

          本文标题:android 点击button状态变化实现分析

          本文链接:https://www.haomeiwen.com/subject/mqqfjttx.html