Details the callback function mechanism in Android

  • 2020-05-24 06:06:49
  • OfStack

Tip: before reading this article, make sure you have a good understanding of the distribution mechanism for Touch events

You will often hear or see the word "callback" during the learning process of Android, so what is a callback? So-called callback function is: in A class defines a method, the method used in an interface and abstract methods in the interface, but there is no specific implementation, abstract methods need B class to implement, B after class implements the method itself won't go to call the method, but is passed to the A class, for A class to call, this mechanism is called a callback.

Below, we take the specific click event of Button for simulation analysis:

First of all, we can find the setOnClickListener(OnClickListener l) method in the View class:


public void setOnClickListener(OnClickListener l) {
     if (!isClickable()) {
         setClickable(true);
     }
     getListenerInfo().mOnClickListener = l;
}

As you can see, OnClickListener is assigned to mOnClickListener in this method, so let's go ahead and see that we implemented onClick() in the performClick() method.


public boolean performClick() {
     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
     ListenerInfo li = mListenerInfo;
     if (li != null && li.mOnClickListener != null) {
         playSoundEffect(SoundEffectConstants.CLICK);
         li.mOnClickListener.onClick(this);
         return true;
     }
     return false;
}

From this we can clearly see that we need to use onClick() method in the parent class, but the parent class does not implement this method, but defines a method setOnClickListener(OnClickListener l). If the subclass wants to be able to respond to click events, it must override the parent class's method and implement OnClickListener interface and its onClick() method. After the subclass implements the interface and method, it is passed to the parent class as a parameter, and the onClick() method is executed in the parent class.

So, why is this method executed in a parent class? That brings us to another important mechanism in Android -- the pass-through mechanism for touch events.

We know that as soon as our finger touches the screen of our phone, 1 will execute dispatchTouchEvent(MotionEvent event). Let's take a look at what dispatchTouchEvent does:


public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
 
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null 
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }
 
            if (onTouchEvent(event)) {
                return true;
            }
        }
 
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
}

We won't go into the details of the Touch distribution mechanism here, because some of the folks on the Internet have already made that clear. Please refer to the link provided at the beginning of this article.

Let's look at line 1, line 17. Since we did not implement OnTouchListener interface, the default return value of onTouch() method is false, so the code in the first if statement will not be executed, so we enter the second if statement and execute onTouchEvent() method. So let's look at the method 1 again:


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);
            }
            // 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:
                    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);
                        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;
    }

The code is too long, let's just focus on the key, in ACTION_UP, case, we found the key code (line 109) : performClick().

At this point, we have pretty much figured out the whole process of the callback mechanism.


Related articles: