Android按键音简单源码分析

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

暂无评论

暂无评论...

随机推荐

USB挂载查看状态简单记录

前言简单的记录一下Android中查看U盘挂载信息。不一定对,只是个人的流水笔记。正文插入U盘,连接Android设备,打开cmd,输入adb shell,然后执行对应命令即可。blkid 信息blkid命令定位/打印块设备属性//部分/dev/block/mmcblk0p24: ...

Ubuntu14编译Android6.0

今天(2016年11月6日)终于编译成功,参考的教程有很多,本来也想写一写,但发现自己可能对编译这东西考虑不全,误导了人,所以只好放弃自己写教程的想法。一:下载Android源码关于这一步,网上有下载好的源码,或者去清华大学开源软件镜像站下载,都很快的。Android6.0源码,网盘下...

聂鲁达:静一静

让我们从一数到十二,然后大家静一静。让我们试一试,在地球上住口不讲任何语言,安静一秒钟,让我们停止动手。想必是神妙的一刻,不慌不忙,没有机车,在瞬息的不安中,让我们互相靠紧。在寒冷的海上,让渔夫停止捕杀鲸鱼,让采盐的人看看自己劳损的手。制造绿色战争的人,制造瓦斯弹烧夷弹...

Notification使用

前言简单记录一下Notification的使用,这里设计简单Notification和自定义View的Notification的使用。记录于此,方便自己查阅和学习。正文简单Notification由于高版本需要添加NotificationChannel,下面就进行了一定的适配。pr...

Android静态换肤-日夜主题切换之不继承Activity

前言记录一下,有Activity换肤之日夜主题无缝切换。一般来说,换肤分为静态换肤和动态换肤,Android的日夜模式可以看做静态换肤的一种。是以资源存放位置来说的,其实不是很严谨,但换肤的本质都是一样的。正文Android高版本都支持日夜模式切换,资源放在对应日夜目录,比如//有...

关闭jaudiotagger的输出日志

前言jaudiotagger.jar是一个非常好的ID3解析,支持多种音频格式解析,比如MP3、FLAC、WAV、M4A等几种。但是解析时会打印很多日志,记录一下,如何关闭日志打印。正文网上也有很多介绍,但效果一般,最后找到如下方式,一行代码关闭所有日志打印。隐藏内容!付费阅读后才能查看...