Android按键音简单源码分析

Android  源码分析  2024年10月24日 am8:32发布1个月前更新 城堡大人
59 0 0

前言

记录一下Android按键音相关分析。

记录于此,方便自己查阅和回顾。

Android P源码上分析

正文

当一个View设置点击监听事件setOnClickListener()时,如果用户点击默认就会有按键音,当然如果你不需要,可以通过如下取消。

  1. xml配置

android:soundEffectsEnabled="false"
  1. 指定View关闭

mView.setSoundEffectsEnabled(false);
  1. 关闭Android所有VIew的按键音

//0是关闭,1是打开
Settings.System.putInt(TestApp.getContext().getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 0);

今天关注上面几种配置分析。

1和2的正对View来说,设置一个属性,点击时判断是否需要响应按键音;3则是在framework层进行判断,是否需要响应。

View.java

下面两个配置一样的原理

android:soundEffectsEnabled="false"
mView.setSoundEffectsEnabled(false);

都给View新增一个标志位(SOUND_EFFECTS_ENABLED & )。我们这里以setSoundEffectsEnabled()进行分析。

\frameworks\base\core\java\android\view\View.java
setSoundEffectsEnabled()
/**
 * View flag indicating whether this view should have sound effects enabled
 * for events such as clicking and touching.
 */
public static final int SOUND_EFFECTS_ENABLED = 0x08000000;
public void setSoundEffectsEnabled(boolean soundEffectsEnabled) {
    setFlags(soundEffectsEnabled ? SOUND_EFFECTS_ENABLED: 0, SOUND_EFFECTS_ENABLED);
}
setFlags()

改变mViewFlags的标志位

void setFlags(int flags, int mask) {
    final boolean accessibilityEnabled =
            AccessibilityManager.getInstance(mContext).isEnabled();
    final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
    int old = mViewFlags;
    mViewFlags = (mViewFlags & ~mask) | (flags & mask);
    int changed = mViewFlags ^ old;
    if (changed == 0) {
        return;
    }
    //略....
}
onTouchEvent()
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
	//略
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            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) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        setPressed(true, x, y);
                    }
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
                        if (!focusTaken) {
						  //按键音流程
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;
            case MotionEvent.ACTION_DOWN:
                //略
                break;
            case MotionEvent.ACTION_CANCEL:
                //略
                break;
            case MotionEvent.ACTION_MOVE:
                //略
                break;
        }
        return true;
    }
    return false;
}

这里重点关注

//初始化mPerformClick
if (mPerformClick == null) {
    mPerformClick = new PerformClick();
}
//执行按键音
if (!post(mPerformClick)) {
    performClickInternal();
}

PerformClick实现Runnable,调用时是执行performClickInternal()的

private final class PerformClick implements Runnable {
    @Override
    public void run() {
        recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
        performClickInternal();
    }
}
post()
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}

如果返回true,表示执行了mPerformClick,否则进入前面的if条件中的performClickInternal()。

其实不论true还是false,最终都是performClickInternal(),只不过调佣的方式不一样而已。

performClickInternal()
private boolean performClickInternal() {
    return performClick();
}
performClick()
public boolean performClick() {
    notifyAutofillManagerOnClick();
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    //重点,需要按键音,这里小设置mOnClickListener监听
    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;
}
playSoundEffect()
public void playSoundEffect(int soundConstant) {
	if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
        return;
    }
    //重点
	mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
}

这里先看isSoundEffectsEnabled()简单的带过,如果mAttachInfo等前面条件满足,那么是否响应按键音,就取决于有没有SOUND_EFFECTS_ENABLED标志。

这就是setSoundEffectsEnabled()设置true和false影响的地方。

@ViewDebug.ExportedProperty
public boolean isSoundEffectsEnabled() {
   return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED);
}

致辞View中影响按键应的分析导致结束了。

下面分析mAttachInfo.mRootCallbacks.playSoundEffect()如何调用到framework层的按键音。

AttachInfo类

回到mAttachInfo。AttachInfo定义也是在View.java内部的。

这里构造函数中倒数第二个参数Callbacks effectPlayer。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
	//这有个接口,重点。上面就是通过接口调用的。
    interface Callbacks {
        void playSoundEffect(int effectId);
        boolean performHapticFeedback(int effectId, boolean always);
    }
	//略
	AttachInfo(IWindowSession session, IWindow window, Display display,
        ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
        Context context) {
		mSession = session;
		mWindow = window;
		mWindowToken = window.asBinder();
		mDisplay = display;
		mViewRootImpl = viewRootImpl;
		mHandler = handler;
		mRootCallbacks = effectPlayer;
		mTreeObserver = new ViewTreeObserver(context);
	}
	//略
}

我们这里只关注按键音,先看AttachInfo初始化的地方。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
	//这里赋值了
    mAttachInfo = info;
	if (mOverlay != null) {
        mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
    }
	//略
}

查看了View代码就是调用dispatchAttachedToWindow()赋值的。

而dispatchAttachedToWindow()调用时在ViewRootImpl.java中。

直接到了ViewRootImpl,具体暂时略过

ViewRootImpl.java

frameworks\base\core\java\android\view\ViewRootImpl.java
performTraversals()

在performTraversals()中,代码很多,这里只展示部分

private void performTraversals() {
    final View host = mView;
	//略
	//第一次
    if (mFirst) {
        //略
		//这里传入mAttachInfo
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
    } else {
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            windowSizeMayChange = true;
        }
    }
    //略
}

而mAttachInfo的初始化是在ViewRootImpl()构造函数中。

public ViewRootImpl(Context context, Display display) {
    //略
    mFirst = true; // true for the first time the view is added
    mAdded = false;
	//mAttachInfo初始化
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
    //略
}

这里重点关注倒数第二个参数(Callbacks effectPlayer)的赋值,这里传入的是this,也就是ViewRootImpl实现了对应的接口。

playSoundEffect()

AttachInfo接口中playSoundEffect()方法在这里实现。

@Override
public void playSoundEffect(int effectId) {
    checkThread();
    try {
        final AudioManager audioManager = getAudioManager();
        switch (effectId) {
            case SoundEffectConstants.CLICK:
            	//调用了audioManager
                audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
                return;
            case SoundEffectConstants.NAVIGATION_DOWN:
                audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
                return;
            case SoundEffectConstants.NAVIGATION_LEFT:
                audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
                return;
            case SoundEffectConstants.NAVIGATION_RIGHT:
                audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
                return;
            case SoundEffectConstants.NAVIGATION_UP:
                audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
                return;
            default:
                throw new IllegalArgumentException("unknown effect id " + effectId +
                        " not defined in " + SoundEffectConstants.class.getCanonicalName());
        }
    } catch (IllegalStateException e) {
        e.printStackTrace();
    }
}

最终还是调用了Framework层的AudioManager

audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
getAudioManager()

audioManager的获取

private AudioManager getAudioManager() {
    if (mView == null) {
        throw new IllegalStateException("getAudioManager called when there is no mView");
    }
    if (mAudioManager == null) {
        mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
    }
    return mAudioManager;
}

AudioManager.java

\frameworks\base\media\java\android\media\AudioManager.java
playSoundEffect()

playSoundEffect()存在多个,但都差不多,我们只关注上面调用的。

public static final int NUM_SOUND_EFFECTS = 10;

public void  playSoundEffect(int effectType) {
	查看是否越界
    if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
        return;
    }
    //查看是否可以播放按键音。
    if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
        return;
    }
    final IAudioService service = getService();
    try {
    	//调用AudioService播放
        service.playSoundEffect(effectType);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}
querySoundEffectsEnabled()
private boolean querySoundEffectsEnabled(int user) {
	int effect = Settings.System.getIntForUser(getContext().getContentResolver(),
            Settings.System.SOUND_EFFECTS_ENABLED, 0, user)
         	//0是关闭,1是打开
    return  effect != 0;
}

这里获取系统设置Settings.System.SOUND_EFFECTS_ENABLED中的配置。

也就是我们开头的方式3的配置,如果配置了0,就是关闭整个系统的按键音。

我们继续分析是如何播放按键音的。

final IAudioService service = getService();
try {
    service.playSoundEffect(effectType);
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

private static IAudioService getService()
{
    if (sService != null) {
        return sService;
    }
    IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
    sService = IAudioService.Stub.asInterface(b);
    return sService;
}

调用的是AudioService.playSoundEffect()

AudioService.java

playSoundEffect()
public void playSoundEffect(int effectType) {
    playSoundEffectVolume(effectType, -1.0f);
}
playSoundEffectVolume()
public void playSoundEffectVolume(int effectType, float volume) {
	//判断系统是否静音
    if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
        return;
    }
    //再次判断effectType是否越界
    if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
        Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
        return;
    }
    //发送MSG_PLAY_SOUND_EFFECT
MSG_PLAY_SOUND_EFFECT消息
    sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT
MSG_PLAY_SOUND_EFFECT消息, SENDMSG_QUEUE,
            effectType, (int) (volume * 1000), null, 0);
}
handleMessage()
 @Override
 public void handleMessage(Message msg) {
 	//略
    case MSG_PLAY_SOUND_EFFECT:
         onPlaySoundEffect(msg.arg1, msg.arg2);
         break;
 	//略
 }
onPlaySoundEffect()
private void onPlaySoundEffect(int effectType, int volume) {
    synchronized (mSoundEffectsLock) {
    	//初始化mSoundPool
        onLoadSoundEffects();
        if (mSoundPool == null) {
            return;
        }
        float volFloat;
        // use default if volume is not specified by caller
        if (volume < 0) {
            volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
        } else {
            volFloat = volume / 1000.0f;
        }
        //播放按键音有两种方式
        //一种是SoundPool,另外一种是MediaPlayer
        //具体使用哪种,要看SOUND_EFFECT_FILES_MAP[effectType][1]的值
        if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
            mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
                                volFloat, volFloat, 0, 0, 1.0f);
        } else {
            MediaPlayer mediaPlayer = new MediaPlayer();
            try {
                String filePath = getSoundEffectFilePath(effectType);
                mediaPlayer.setDataSource(filePath);
                mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
                mediaPlayer.prepare();
                mediaPlayer.setVolume(volFloat);
                mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                    public void onCompletion(MediaPlayer mp) {
                        cleanupPlayer(mp);
                    }
                });
                mediaPlayer.setOnErrorListener(new OnErrorListener() {
                    public boolean onError(MediaPlayer mp, int what, int extra) {
                        cleanupPlayer(mp);
                        return true;
                    }
                });
                mediaPlayer.start();
            } catch (IOException ex) {
                Log.w(TAG, "MediaPlayer IOException: "+ex);
            } catch (IllegalArgumentException ex) {
                Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
            } catch (IllegalStateException ex) {
                Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
            }
        }
    }
}

至此,按键音跟踪就结束了。

参考文章

 历史上的今天

  1. 2023: WordPress常用的函数介绍(0条评论)
  2. 2019: 苏童:我从来不敢夸耀童年的幸福(0条评论)
版权声明 1、 本站名称: 91易搜
2、 本站网址: https://www.91es.com/
3、 本站部分文章来源于网络,仅供学习与参考,如有侵权,请留言

暂无评论

暂无评论...

随机推荐

拷贝文件时异常断电导致文件拷贝失败

前言测试反馈如果断电前点击收藏歌曲(也就是拷贝到内置存储卡,有提示拷贝成功)后,开机起来后小概率出现部分收藏的歌曲消失了。原因大概知道,但讲得不够清晰,因此看到网友文章,摘抄于此,方便自己查阅。真正原因:拷贝文件是写入缓冲区内,调用的flush()也无法保证文件写入到了物理存储设备上。PS...

Android 换肤setFactory2时出现No field mFactorySet in class问题解决

前言Android换肤插件开发时,需要依赖LayoutInflater.Factory类,在Android P和 之前版本都测试ok,但放在Android Q(10)上测试就失效了。本文记录一下失效的原因和解决的方法。正文在Android Q之前版本,通过如下代码可以设置的 try {...

柴静:真相常流失于涕泪交加中

一美国有一个著名的白宫记者,叫海伦.托马斯,逼问过9任总统,进攻性极强,后来白宫特别在新闻厅给她专门设了把椅子,上面用小铜牌刻着她名字,又用她的名字命名了一个奖项,盛誉极隆。她八十多岁的时候在书里回忆自己职业生涯,曾经感叹美国新闻业的萧条,说“不知畏惧,不带好恶地去报道,美国的新闻人忘了吗?”...

删除开机动画bootanimation

推荐使用 读书导航 极客导航 :125啦极客导航(http://www.biumall.com/jike.html)Android开机动画有2种模式,一种是使用两张图片利用某种效果来造成动态,另一种则是用一个图包循环显示的方式来形成动态。当然,这时因为mediaplayer已经起来,播放...

周国平:爱的距离

好的爱情有韧性,拉得开,但又扯不断。相爱者互不束缚对方,是他们对爱情有信心的表现。谁也不限制谁,到头来仍然是谁也离不开谁,这才是真爱。好的两性关系有弹性,彼此既非僵硬地占有,也非软弱地依附。相爱的人给予对方的最好礼物是自由。两个自由人之间的爱,拥有必要的张力。这种爱牢固,但不板结;缠绵,但不黏滞...

TBox、ECall、BCall、ICall

前言本章简单介绍一下TBox、ECall、BCall、ICall的专用名称和其作用。正文TBoxT-Box称为车载智能终端,作为车身唯一可以联网的控制单元,肩负着监控和控制车身状态的使命,其存在的最大价值就在与网络的连接性。TBOX上通云端TSP(Telematics Service Pr...