【安卓R 源码】Ripple 水波纹效果源码

安卓使用ripple实现点击时的涟漪效果 - 简书

https://www.jb51.net/article/145309.htm

Android:RippleDrawable 水波纹/涟漪效果 - 简书

Android5.0 水波控件RippleDrawable简析_Jeepend的专栏-CSDN博客

RippleDrawable水波绘制分析 - 掘金

自定义Drawable实现点击水波纹涟漪效果_Eshel的博客-CSDN博客

自定义一个水波纹 xml:

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/colorPrimary">
    <item
        android:id="@android:id/mask"
        android:drawable="@android:color/white" />

    <item android:drawable="@color/cccccc" />

</ripple>

在view 中使用:

我们再来梳理下绘制流程:

  • RippleDrawableinflate过程初始化了一层层的layer,添加到LayerState里面,初始化mask部分的drawable,放到了mMask全局drawable里面,初始化了ripple标签里面的color属性。
  • 在RippleDrawable静态绘制部分先是绘制了非id=mask的item
  • mask部分color属性值alpha=255是不会绘制的,因此颜色值的alpha值需要在[0,255)这个区间,mask绘制是在rippleForeground和RippleBackground的绘制下层。
  • 接着绘制RippleBackground部分,如果RippleBackground.isVisible才绘制。
  • 接着绘制每次exit未完成的RippleForeground部分,注意这里是个集合遍历绘制RippleForeground
  • 接着才是绘制当前次的RippleForeground
  • 在动画部分,先是触发了RippleDrawableonStateChange方法,接着创建了RippleForeground,调用了RippleForegroundentersetup``方法,在enter里面创建了softWare动画,其中hardWare动画是要开启了硬件加速功能才能创建,所以默认不会创建softWare`动画。
  • RippleForeground中的softWare创建的动画有三个,一个是半径、圆心、透明度变化的三个动画,在enter的时候RippleForegroundRippleDrawable.isBounded的时候不创建动画;在exit的时候不会限制创建动画,这个是在android-27下面的源码。在android-28的手机上面我看下了效果是在enter的时候有水波动画,exit的时候没有动画,大家可以用android-28的手机尝试下。
  • RippleBackground中就一个动画,改变画笔的透明底,enter情况下画笔从0到1的过程;在exit的时候画笔的透明度先是从1到0,然后又从0到1的过程。
  • 上面提到的enterexit中的动画,都是不断地调用到RippleDrawableinvalidateSelf方法,而invalidateSelf会触发viewdraw方法,最后触发了RippleDrawabledraw方法,最终会触发到RippleForegrounddrawSoftwareRippleBackgrounddrawSoftware
  • RippleDrawable中动画销毁是在view#dispatchdetachedFromWindowRippleDrawablejumpToCurrentState方法。

其中:RippleForground 与 RippleBackground 的区别

RippleForground负责水波的绘制,RippleBackground负责绘制透明度渐变的动画

1) RippleDrawable的绘制

关于drawable的绘制,直接看RippleDrawable的draw方法:

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (mState.mRippleStyle == STYLE_SOLID) {
            drawSolid(canvas);
        } else {
            drawPatterned(canvas);
        }
    }
---------------

    private void drawSolid(Canvas canvas) {
        pruneRipples();

        // Clip to the dirty bounds, which will be the drawable bounds if we
        // have a mask or content and the ripple bounds if we're projecting.
        final Rect bounds = getDirtyBounds();
//先保存canvas的状态
        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        if (isBounded()) {
//裁剪drawable的区域
            canvas.clipRect(bounds);
        }
//绘制content部分
        drawContent(canvas);
//绘制波纹部分
        drawBackgroundAndRipples(canvas);
// 还原canvas的状态
        canvas.restoreToCount(saveCount);
    }

直接绘制item的id不是mask的drawable。在开篇的事例中,不带id=mask的drawable="#cccccc",此处是一个colorDrawable。

private void drawContent(Canvas canvas) {
    // Draw everything except the mask.
    final ChildDrawable[] array = mLayerState.mChildren;
    final int count = mLayerState.mNumChildren;
    for (int i = 0; i < count; i++) {
        if (array[i].mId != R.id.mask) {
            array[i].mDrawable.draw(canvas);
        }
    }
}

这部分是波纹效果的关键,看下drawBackgroundAndRipples方法:

    private void drawBackgroundAndRipples(Canvas canvas) {
//绘制水波的动画类
        final RippleForeground active = mRipple;
//绘制背景的动画类
        final RippleBackground background = mBackground;
//抬起的次数
        final int count = mExitingRipplesCount;
        if (active == null && count <= 0 && (background == null || !background.isVisible())) {
            // Move along, nothing to draw here.
            return;
        }
//获取到点击时的坐标
        final float x = mHotspotBounds.exactCenterX();
        final float y = mHotspotBounds.exactCenterY();
//将画布偏移到点击的坐标位置
        canvas.translate(x, y);

        final Paint p = getRipplePaint();

//如果background不为空,并且isVisible才去绘制background
        if (background != null && background.isVisible()) {
            background.draw(canvas, p);
        }

//将每一次exit的ripple依次绘制出来,可以看出来该处是绘制波纹效果的关键
        if (count > 0) {
            final RippleForeground[] ripples = mExitingRipples;
            for (int i = 0; i < count; i++) {
                ripples[i].draw(canvas, p);
            }
        }

//当前次的rippleForeground绘制
        if (active != null) {
            active.draw(canvas, p);
        }

        canvas.translate(-x, -y);
    }

2) 取消动画流程:

  • frameworks/base/core/java/android/view/View.java
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchDetachedFromWindow() {
        AttachInfo info = mAttachInfo;
        if (info != null) {
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(GONE);
                if (isShown()) {
                    // Invoking onVisibilityAggregated directly here since the subtree
                    // will also receive detached from window
                    onVisibilityAggregated(false);
                }
            }
        }

    //一般自定义view的时候重写该方法,比如释放动画等等
    onDetachedFromWindow();
    //销毁drawable的地方
    onDetachedFromWindowInternal();

------------
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected void onDetachedFromWindowInternal() {
        mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
        mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;

        removeUnsetPressCallback();
        removeLongPressCallback();
        removePerformClickCallback();
        clearAccessibilityThrottles();
        stopNestedScroll();

        // Anything that started animating right before detach should already
        // be in its final state when re-attached.
        jumpDrawablesToCurrentState();

-------------
public void jumpDrawablesToCurrentState() {
    if (mBackground != null) {
        mBackground.jumpToCurrentState();
    }
    if (mStateListAnimator != null) {
        mStateListAnimator.jumpToCurrentState();
    }
    if (mDefaultFocusHighlight != null) {
        mDefaultFocusHighlight.jumpToCurrentState();
    }
    if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
        mForegroundInfo.mDrawable.jumpToCurrentState();
    }
}

都是调用了drawable的jumpToCurrentState方法,直接来到RippleDrawable下面的该方法:

  • frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java
    @Override
    public void jumpToCurrentState() {
        super.jumpToCurrentState();

        if (mRipple != null) {
            mRipple.end();
        }

        if (mBackground != null) {
            mBackground.jumpToFinal();
        }

        cancelExitingRipples();
        endPatternedAnimations();
    }

------------------
    private void cancelExitingRipples() {
        final int count = mExitingRipplesCount;
        final RippleForeground[] ripples = mExitingRipples;
        for (int i = 0; i < count; i++) {
            ripples[i].end();
        }

        if (ripples != null) {
            Arrays.fill(ripples, 0, count, null);
        }
        mExitingRipplesCount = 0;
        // Always draw an additional "clean" frame after canceling animations.
        invalidateSelf(false);
    }

调用了RippleForegroundendRippleBackgroundend以及在cancelExitingRipples方法里面调用了每次exit未完成的RippleForeground的end方法

  • frameworks/base/graphics/java/android/graphics/drawable/RippleForeground.java
     * Ends all animations, jumping values to the end state.
     */
    public void end() {
        for (int i = 0; i < mRunningSwAnimators.size(); i++) {
            mRunningSwAnimators.get(i).end();
        }
        mRunningSwAnimators.clear();
        for (int i = 0; i < mRunningHwAnimators.size(); i++) {
            mRunningHwAnimators.get(i).end();
        }
        mRunningHwAnimators.clear();
    }

3)触发 RippleForeground 的动画

由下面代码分析进行如下炒作:

        mRipple.setup(mState.mMaxRadius, mDensity);
        mRipple.enter();

setup 时调用父类方法

  • frameworks/base/graphics/java/android/graphics/drawable/RippleComponent.java
    public final void setup(float maxRadius, int densityDpi) {
        if (maxRadius >= 0) {
            mHasMaxRadius = true;
            mTargetRadius = maxRadius;
        } else {
            mTargetRadius = getTargetRadius(mBounds);
        }

        mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;

        onTargetRadiusChanged(mTargetRadius);
    }

// getTargetRadius里面通过勾股定理得到view大小的对角线的一半。最后调用了onTargetRadiusChanged方法,该方法是个空方法,可以想到是交给子类自己去处理mTargetRadius的问题

    private static float getTargetRadius(Rect bounds) {
        final float halfWidth = bounds.width() / 2.0f;
        final float halfHeight = bounds.height() / 2.0f;
        return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    }

enter 方法,绘制动画:

     * Starts a ripple enter animation.
     */
    public final void enter() {
        mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis();
// 软件加速绘制
        startSoftwareEnter();
// 硬件加速绘制
        startHardwareEnter();
    }
------------
    private void startSoftwareEnter() {
        for (int i = 0; i < mRunningSwAnimators.size(); i++) {
            mRunningSwAnimators.get(i).cancel();
        }
        mRunningSwAnimators.clear();

//radius动画
        final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
        tweenRadius.setDuration(RIPPLE_ENTER_DURATION);
        tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
        tweenRadius.start();
        mRunningSwAnimators.add(tweenRadius);

//水波画圆的时候圆心动画,从点击的点到rippleDrawable中心位置一直到点击的点到rippleDrawable中心位置的0.7的圆心渐变动画
        final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
        tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION);
        tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
        tweenOrigin.start();
        mRunningSwAnimators.add(tweenOrigin);

//透明度的动画
        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
        opacity.setDuration(OPACITY_ENTER_DURATION);
        opacity.setInterpolator(LINEAR_INTERPOLATOR);
        opacity.start();
        mRunningSwAnimators.add(opacity);
    }
  • tweenRadius定义水波画圆的时候半径的动画
  • tweenOrigin定义水波画圆的时候圆心的动画
  • opacity定义水波透明度的动画、 上面三个动画都用到了动画的Property形式实现当前类值的改变,都是从0到1的过程,在tweenRadius动画中不断改变RippleForeground中的mTweenRadius变量,在tweenOrigin动画中不断改变mTweenXmTweenX全局变量,opacity动画中不断改变mOpacity全局变量。并且在动画的setValue方法中都会调用invalidateSelf方法,最终会重新调用到rippleDrawable的invalidateSelf方法,在第一节中简单提过invalidateSelf方法,最终会触发drawable的draw方法,因此可以想到实际上rippleForeground中的动画会不断draw方法:
  • frameworks/base/graphics/java/android/graphics/drawable/RippleForeground.java
    public void draw(Canvas c, Paint p) {
        final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof RecordingCanvas;

        pruneSwFinished();
        if (hasDisplayListCanvas) {
            final RecordingCanvas hw = (RecordingCanvas) c;
            drawHardware(hw, p);
        } else {
// 如果没开启硬件加速,hardWare动画是没有打开的,因此直接看drawSoftware部分
            drawSoftware(c, p);
        }
    }

-----------------
    private void drawSoftware(Canvas c, Paint p) {
//获取到画笔最开始的透明度,透明度是ripple标签color颜色值透明度的一半
        final int origAlpha = p.getAlpha();
        final int alpha = (int) (origAlpha * mOpacity + 0.5f);
//获取到当前的圆的半径
        final float radius = getCurrentRadius();
        if (alpha > 0 && radius > 0) {
            final float x = getCurrentX();
            final float y = getCurrentY();
            p.setAlpha(alpha);
            c.drawCircle(x, y, radius, p);
            p.setAlpha(origAlpha);
        }
    }

上面通过mOpacity算出当前画笔的透明度,这里用了一个+0.5f转成int类型,这个是很常用的float转int类型的计算方式吧,通常在现有基础上+0.5f。mOpacity变量是在opacity动画中通过它的property改变全局属性的方式,关于动画大家可以看看property的使用,这里用到的是FloatProperty的类型:

/**
 * Property for animating opacity between 0 and its target value.
 */
private static final FloatProperty<RippleForeground> OPACITY =
        new FloatProperty<RippleForeground>("opacity") {
    @Override
    public void setValue(RippleForeground object, float value) {
        object.mOpacity = value;
        object.invalidateSelf();
    }
    @Override
    public Float get(RippleForeground object) {
        return object.mOpacity;
    }
};

时序图如下:

1. 水波纹效果执行流程

如果自定义了View,会响应ontouch 点击事件:

  • frameworks/base/core/java/android/view/View.java
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {

// 这里有个 setPressed
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {

// 如果是向下点击事件的话
                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }

                    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();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

// 否则,设置点击是 true
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

    private void setPressed(boolean pressed, float x, float y) {
        if (pressed) {

// xy是点击的焦点
            drawableHotspotChanged(x, y);
        }
// 设置为 true
        setPressed(pressed);
    }


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

刷新图标的状态

    public void refreshDrawableState() {
        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;

// 绘制图标状态变化
        drawableStateChanged();

        ViewParent parent = mParent;
        if (parent != null) {
            parent.childDrawableStateChanged(this);
        }
    }

    protected void drawableStateChanged() {
        final int[] state = getDrawableState();
        boolean changed = false;

// 看后台的Drawable 、前台的是否不为空 isStateful,如果不是,则去设置状态
        final Drawable bg = mBackground;
        if (bg != null && bg.isStateful()) {
            changed |= bg.setState(state);
        }

        final Drawable hl = mDefaultFocusHighlight;
        if (hl != null && hl.isStateful()) {
            changed |= hl.setState(state);
        }

        final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (fg != null && fg.isStateful()) {
            changed |= fg.setState(state);
        }

        if (mScrollCache != null) {
            final Drawable scrollBar = mScrollCache.scrollBar;
            if (scrollBar != null && scrollBar.isStateful()) {
                changed |= scrollBar.setState(state)
                        && mScrollCache.state != ScrollabilityCache.OFF;
            }
        }

        if (mStateListAnimator != null) {
            mStateListAnimator.setState(state);
        }

        if (!isAggregatedVisible()) {
            // If we're not visible, skip any animated changes
            jumpDrawablesToCurrentState();
        }

        if (changed) {
            invalidate();
        }
    }

调用 Drawable 去setState 设置状态

  • frameworks/base/graphics/java/android/graphics/drawable/Drawable.java
// 这是子类 RippleDrawable 调用父类的 setState方法
    public boolean setState(@NonNull final int[] stateSet) {
        if (!Arrays.equals(mStateSet, stateSet)) {
            mStateSet = stateSet;
            return onStateChange(stateSet);
        }
        return false;
    }

// 其对应会走到子类的 onStateChange

    protected boolean onStateChange(int[] state) {
        return false;
    }

调用 子类 RippleDrawable 的 onStateChanged 方法:

  • frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java

onStateChange逻辑很清晰,在enable并且pressed状态下会触发setRippleActivesetBackgroundActive方法 

    @Override
    protected boolean onStateChange(int[] stateSet) {
        final boolean changed = super.onStateChange(stateSet);

// 这里通过获取 stateSet 数组,得到对应的点击事件
        boolean enabled = false;
        boolean pressed = false;
        boolean focused = false;
        boolean hovered = false;

        for (int state : stateSet) {
            if (state == R.attr.state_enabled) {
                enabled = true;
            } else if (state == R.attr.state_focused) {
                focused = true;
// 如果有点击的状态的话
            } else if (state == R.attr.state_pressed) {
                pressed = true;
            } else if (state == R.attr.state_hovered) {
                hovered = true;
            }
        }
// 设置动态波纹效果的激活的
        setRippleActive(enabled && pressed);
        setBackgroundActive(hovered, focused, pressed);

        return changed;
    }
=============
    private void setRippleActive(boolean active) {
        if (mRippleActive != active) {
            mRippleActive = active;
// 极大可能走这里
            if (mState.mRippleStyle == STYLE_SOLID) {
                if (active) {
// //按下的时候调用该方法
                    tryRippleEnter();
                } else {
//抬起的时候调用该方法
                    tryRippleExit();
                }
            } else {

                if (active) {
                    startPatternedAnimation();
                } else {
                    exitPatternedAnimation();
                }
            }
        }
    }

=============
     * Attempts to start an enter animation for the active hotspot. Fails if
     * there are too many animating ripples.
     */
    private void tryRippleEnter() {
//限制了ripple最大的次数
        if (mExitingRipplesCount >= MAX_RIPPLES) {
            // This should never happen unless the user is tapping like a maniac
            // or there is a bug that's preventing ripples from being removed.
            return;
        }

        if (mRipple == null) {
            final float x;
            final float y;
            if (mHasPending) {
按下时候的坐标
                mHasPending = false;
                x = mPendingX;
                y = mPendingY;
            } else {
                x = mHotspotBounds.exactCenterX();
                y = mHotspotBounds.exactCenterY();
            }
//生成了一个RippleForeground
            mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware);
        }
//紧接着调用了setUp和enter方法
        mRipple.setup(mState.mMaxRadius, mDensity);
        mRipple.enter();
    }

2. 自定义水波纹效果

自定义Drawable实现点击水波纹涟漪效果_Eshel的博客-CSDN博客

既然是自定义 Drawable 实现,那么有如下问题需要解答:

  • 在 Drawable 里怎么监听用户点击事件? 难道要外部调用吗? 系统可没有这么干
  • 怎么监听点击的位置? (明显系统的效果是从点击位置开始扩散的)
  • 水波纹效果分析, 包含哪些动画?

退出的动画

自定义的明显比系统的生硬,仔细观察系统波纹效果,其实抬起的时候是有一个渐变的动画的,接下来就来实现这个渐变动画。

private void exit() {
	//如果正在执行扩散动画, 需要等待动画执行完毕再执行退出动画
	if(progress != maxProgress && mState == STATE_ENTER){
		pendingExit = true;
	}else {
		mState = STATE_EXIT;
		startExitAnimation();
	}
}

private void startExitAnimation() {
	if(mRunningAnimator != null && mRunningAnimator.isRunning()){
		mRunningAnimator.cancel();
	}
	mRunningAnimator = ValueAnimator.ofInt(maxProgress, 0);
	mRunningAnimator.setInterpolator(new LinearInterpolator());
	mRunningAnimator.setDuration(animationTime);//300ms
	mRunningAnimator.addUpdateListener(animation -> {
		progress = (int) animation.getAnimatedValue();
		invalidateSelf();
	});
	mRunningAnimator.start();
}

@Override
public void draw(@NonNull Canvas canvas) {
	if(mState == STATE_ENTER){
		if(mPaint.getAlpha() != mRealAlpha){
			mPaint.setAlpha(mRealAlpha);
		}
		canvas.drawCircle(mPressedPointF.x, mPressedPointF.y, mMaxRadius * progress / maxProgress, mPaint);
	}else if(mState == STATE_EXIT){
		mPaint.setAlpha(mRealAlpha * progress / maxProgress);
		canvas.drawRect(getBounds(), mPaint);
	}
}

3. 点击 Buttom 回调 onClick 流程

这里写图片描述

 在activity 中 自定义的点击响应事件:

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

            }
        });

其中:View.OnClickListener,  是View的一个内部接口,只有一个实现方法。

//源码
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    /**
     * Interface definition for a callback to be invoked when a view is clicked.
     */
    public interface OnClickListener {
        /**
         * Called when a view has been clicked.
         *
         * @param v The view that was clicked.
         */
        void onClick(View v);
    }
}

实现为:这里要设置 setOnClickListener 点击的监听器


public class TestA implements View.OnClickListener {
 
// 这里要设置 setOnClickListener
    button.setOnClickListener(this);
    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.test1:
                //BALBALA
                break;
            case
            ...
        }
    }
}

看一下 button 的继承链:

  • frameworks/base/core/java/android/widget/Button.java
public class Button extends TextView {
   
   
  • frameworks/base/core/java/android/widget/TextView.java
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    static final String LOG_TAG = "TextView";
  • frameworks/base/core/java/android/view/View.java
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private static final boolean DBG = false;

// 调用父类 View的类,设置监听器为 OnClickListener ,赋值给mOnClickListener 
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
//一个view组件含多个Listener监听器,这个只赋值click,其他的还有OnFocusChangeListener、OnLongClickListener等
        getListenerInfo().mOnClickListener = l;
    }

当有点击事件触发的时候,有up 事件,则去响应click 事件:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
// 响应 up事件
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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();
                                }
// post 增加到消息队列中
                                if (!post(mPerformClick)) {
// 响应点击事件
                                    performClickInternal();
                                }
                            }
                        }

                        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();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
// 响应 down 点击事件
                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

performClickInternal

    private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }

---------
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;

// 如果观察者不为空的话,回调响应 onclick 事件
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

猜你喜欢

转载自blog.csdn.net/qq_40587575/article/details/122355699