https://www.jb51.net/article/145309.htm
Android:RippleDrawable 水波纹/涟漪效果 - 简书
Android5.0 水波控件RippleDrawable简析_Jeepend的专栏-CSDN博客
自定义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 中使用:
我们再来梳理下绘制流程:
RippleDrawable
在inflate
过程初始化了一层层的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
。- 在动画部分,先是触发了
RippleDrawable
的onStateChange
方法,接着创建了RippleForeground
,调用了RippleForeground
的enter
和setup``方法,在enter里面创建了
softWare动画,其中
hardWare动画是要开启了硬件加速功能才能创建,所以默认不会创建
softWare`动画。RippleForeground
中的softWare
创建的动画有三个,一个是半径、圆心、透明度变化的三个动画,在enter
的时候RippleForeground
在RippleDrawable.isBounded
的时候不创建动画;在exit
的时候不会限制创建动画,这个是在android-27
下面的源码。在android-28
的手机上面我看下了效果是在enter
的时候有水波动画,exit
的时候没有动画,大家可以用android-28
的手机尝试下。RippleBackground
中就一个动画,改变画笔的透明底,enter
情况下画笔从0到1的过程;在exit
的时候画笔的透明度先是从1到0,然后又从0到1的过程。- 上面提到的
enter
和exit
中的动画,都是不断地调用到RippleDrawable
的invalidateSelf
方法,而invalidateSelf
会触发view
的draw
方法,最后触发了RippleDrawable
的draw
方法,最终会触发到RippleForeground
的drawSoftware
和RippleBackground
的drawSoftware
。- RippleDrawable中动画销毁是在
view#dispatchdetachedFromWindow
到RippleDrawable
的jumpToCurrentState
方法。
其中: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);
}
调用了RippleForeground
的end
、RippleBackground
的end
以及在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
动画中不断改变mTweenX
和mTweenX
全局变量,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状态下会触发setRippleActive
和setBackgroundActive
方法
@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;
}