Android按键音简单源码分析

Android  源码分析  2024年10月24日 am8:32发布3天前更新 91es.com站长
3 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、 本站网址: 91es.com3xcn.com
3、 本站内容: 部分来源于网络,仅供学习和参考,若侵权请留言
3、 本站申明: 个人流水账日记,内容并不保证有效

暂无评论

暂无评论...

随机推荐

Android Socket之服务端封装

前言之前记录了Android中Socket的简单使用,也就是对数据的简单收发操作。这简单的对SocketServer进行封装。记录于此,方便自己查阅。正文SocketServer服务端一般不需要我们写,大多数是作为客户端去绑定其他的,比如Tbox。但服务端跟客户端差不多,因此也一起整理一下...

Android设备adb screenrecord录屏简单使用

前言Android测试有些现象无法用语言描述或者描述不清晰,因此会要求测试拍视频或者录屏,方便工程师解决问题。偶尔需要录像,因此这里整理一下adb screenrecord的使用,方便自己查阅和学习。好记性不如烂笔头正文screenrecord 命令规则screenrecord [op...

泰戈尔:你一定要走吗?

旅人,你一定要走吗?夜是静谧的,黑暗昏睡在树林上。露台上灯火辉煌,繁花朵朵鲜丽,年轻的眼睛也还是清醒的。旅人,你一定要走吗?我们不曾以恳求的手臂束缚你的双足,你的门是开着的,你的马上了鞍子站在门口。如果我们设法挡住你的去路,那也不过是用我们的歌声罢了,如果我们曾设法挡住你,那也不过是用...

Android String根据指定长度进行截取文本

前言在做自定义TextImageView时,画的文本长度存在过长,因此需要跟进ImageView的宽度进行限制Text的宽度,也就引出需要新需求:根据Text文本长度进行裁剪PS: Button 本来是支持图片+Text的组合显示的,但是Glide需要传入ImageView,因此才有TextI...

JobIntentService的使用

前言简单记录一下JobIntentService的使用,后面高版本又废弃了,推荐使用Android Jetpack了。虽然废弃了,但还是记录一下(之前没记录)。水文一篇,哈哈,大佬们可以跳过。正文Android 8.0对系统资源的管控更加严格,添加了后台限制规则。如果满足以下任意条件...

Android最近任务预览页面,概览屏幕预览图修改,敏感信息预览图隐藏实现

前言这个需求是显示在最近任务栏,但不让人预览。正文关于这个问题,其实让人很恼火,为啥不让人看呢?不过需求贵需求,作为程序猿,我们的任务就是为了完成各种奇葩的需求。下面分享几种可以实现的方式。使用FLAG_SECURE在Activity中的onCreate()中 设置窗口的Flags属...