一、按键音调用流程

摘要:按键音播放的总体逻辑是先找到系统中按键音的资源,然后调用SoundPool.load让系统加载音频资源,加载成功后在onLoadComplete回调中会返回一个非0的soundID ,用于播放时指定特定的音频,最后在需要播放按键音的时候直接根据soundID播放

1.Android按键音接口

Android按键音只有两个常用接口,分别是:

  1. 原生设置APP中SoundFragment.java调用的设置按键音开关的接口:mAudioManager.loadSoundEffects()和mAudioManager.unloadSoundEffects()
    private void setSoundEffectsEnabled(boolean enabled) {mAudioManager = (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); //1if (enabled) {mAudioManager.loadSoundEffects();   } else {mAudioManager.unloadSoundEffects();}Settings.System.putInt(getActivity().getContentResolver(),Settings.System.SOUND_EFFECTS_ENABLED, enabled ? 1 : 0);}

先调用AudioManager的loadSoundEffects方法,然后会调用到AudioService的loadSoundEffects方法

  1. View.java中播放按键音的接口:playSoundEffect
    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;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK);//调用会经过ViewRootImpl.java,最终调用到AudioService中li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}

最终会调用到AudioService的playSoundEffect方法

2.onLoadSoundEffects()方法

上述的两个方法调用到AudioService之后,分别通过sendMsg向handler发送MSG_LOAD_SOUND_EFFECTS和MSG_PLAY_SOUND_EFFECT信息,handler在收到信息后会进行相应的操作,但是不管是哪个操作,都会调用到onLoadSoundEffects()方法

loadSoundEffects的调用流程(非重点):

    public boolean loadSoundEffects() {int attempts = 3;LoadSoundEffectReply reply = new LoadSoundEffectReply();synchronized (reply) {//调用sendMsg方法sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);while ((reply.mStatus == 1) && (attempts-- > 0)) {try {reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);} catch (InterruptedException e) {Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded.");}}}return (reply.mStatus == 0);}//sendMsg方法是对handler.sendMessageAtTime的封装private static void sendMsg(Handler handler, int msg,int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {if (existingMsgPolicy == SENDMSG_REPLACE) {handler.removeMessages(msg);} else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {return;}synchronized (mLastDeviceConnectMsgTime) {long time = SystemClock.uptimeMillis() + delay;if (msg == MSG_SET_A2DP_SRC_CONNECTION_STATE ||msg == MSG_SET_A2DP_SINK_CONNECTION_STATE ||msg == MSG_SET_HEARING_AID_CONNECTION_STATE ||msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE ||msg == MSG_A2DP_DEVICE_CONFIG_CHANGE ||msg == MSG_BTA2DP_DOCK_TIMEOUT) {if (mLastDeviceConnectMsgTime >= time) {// add a little delay to make sure messages are ordered as expectedtime = mLastDeviceConnectMsgTime + 30;}mLastDeviceConnectMsgTime = time;}handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);}}//在handleMessage中处理消息
@Overridepublic void handleMessage(Message msg) {......case MSG_PLAY_SOUND_EFFECT://调用onPlaySoundEffectonPlaySoundEffect(msg.arg1, msg.arg2);break;
}private void onPlaySoundEffect(int effectType, int volume) {synchronized (mSoundEffectsLock) {//最终会调用到onLoadSoundEffectsonLoadSoundEffects();......}

playSoundEffect的调用流程(非重点):

    public void playSoundEffect(int effectType) {playSoundEffectVolume(effectType, -1.0f);}public void playSoundEffectVolume(int effectType, float volume) {// do not try to play the sound effect if the system stream is mutedif (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {return;}if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {Log.w(TAG, "AudioService effectType value " + effectType + " out of range");return;}sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,effectType, (int) (volume * 1000), null, 0);}//在handleMessage中处理消息
@Overridepublic void handleMessage(Message msg) {......case MSG_PLAY_SOUND_EFFECT:onPlaySoundEffect(msg.arg1, msg.arg2);break;
}private void onPlaySoundEffect(int effectType, int volume) {synchronized (mSoundEffectsLock) {onLoadSoundEffects();......}

如上所述最终都会调用onLoadSoundEffects方法
在onLoadSoundEffects方法中主要完成以下几件事:

  1. 调用loadTouchSoundAssets方法解析XML文件,获得音源文件名,初始化数组,将音源文件与数组中元素一一对应
  2. 补全音源文件路径,调用SoundPool.load方法
  3. 将SoundPool.load方法返回的sampleId保存在数组中,作为之后play方法的参数

先来看loadTouchSoundAssets方法,代码如下:

    private void loadTouchSoundAssets() {XmlResourceParser parser = null;// only load assets once.//SOUND_EFFECT_FILES是一个存放字符串的List,里面存放的是音频资源的名称if (!SOUND_EFFECT_FILES.isEmpty()) {return;}//此方法执行://1.SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); 向SOUND_EFFECT_FILES添加一个音频资源的名称//2.初始化一个二维数组SOUND_EFFECT_FILES_MAP。行数为10,列数为2,第一列都为0,第二列都为-1loadTouchSoundAssetDefaults();try {//获得XML对象parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets);XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS);//getAttributeValue方法用于获取传入的Attribute名称对应的Value,这里是"1.0"String version = parser.getAttributeValue(null, ATTR_VERSION);boolean inTouchSoundsGroup = false;if (ASSET_FILE_VERSION.equals(version)) {while (true) {//nextElement方法用于切换到XML的下一层XmlUtils.nextElement(parser);//获取当前parser的名称,这里是"group"String element = parser.getName();if (element == null) {break;}if (element.equals(TAG_GROUP)) {String name = parser.getAttributeValue(null, ATTR_GROUP_NAME);if (GROUP_TOUCH_SOUNDS.equals(name)) {inTouchSoundsGroup = true;break;}}}//遍历XML中剩下的所有元素while (inTouchSoundsGroup) {XmlUtils.nextElement(parser);String element = parser.getName();if (element == null) {break;}if (element.equals(TAG_ASSET)) {String id = parser.getAttributeValue(null, ATTR_ASSET_ID);String file = parser.getAttributeValue(null, ATTR_ASSET_FILE);int fx;try {//getField的对象是.class文件(.java文件的预编译产物,只进行一些变量即宏的替换),这里即AudioManager.class//根据传入的id获得AudioManager.class中对应的对象,例如传入的是"FX_KEY_CLICK",得到的是AudioManager中定义的public static final int FX_KEY_CLICK = 0Field field = AudioManager.class.getField(id);fx = field.getInt(null);} catch (Exception e) {Log.w(TAG, "Invalid touch sound ID: "+id);continue;}//根据之前XML中读取的file取出其在SOUND_EFFECT_FILES的位置//此时SOUND_EFFECT_FILES只有一个元素即"Effect_Tick.ogg"//如果不存在则加入到SOUND_EFFECT_FILES中int i = SOUND_EFFECT_FILES.indexOf(file);if (i == -1) {i = SOUND_EFFECT_FILES.size();SOUND_EFFECT_FILES.add(file);}SOUND_EFFECT_FILES_MAP[fx][0] = i;} else {break;}}}} catch (Resources.NotFoundException e) {Log.w(TAG, "audio assets file not found", e);} catch (XmlPullParserException e) {Log.w(TAG, "XML parser exception reading touch sound assets", e);} catch (IOException e) {Log.w(TAG, "I/O exception reading touch sound assets", e);} finally {if (parser != null) {parser.close();}}}private void loadTouchSoundAssetDefaults() {SOUND_EFFECT_FILES.add("Effect_Tick.ogg");for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) {SOUND_EFFECT_FILES_MAP[i][0] = 0;SOUND_EFFECT_FILES_MAP[i][1] = -1;}}

经过loadTouchSoundAssets初始化后,SOUND_EFFECT_FILES数组为:

{ "Effect_Tick.ogg" , "KeypressStandard.ogg" , "KeypressSpacebar.ogg" ,"KeypressDelete.ogg" , "KeypressReturn.ogg" , "KeypressInvalid.ogg" }

SOUND_EFFECT_FILES_MAP数组为:

{{0, -1}, {0, -1}, {0, -1}, {0, -1}, {0, -1}, {1, -1}, {2, -1}, {3, -1}, {4, -1}, {5, -1}}

再来看真正的onLoadSoundEffects方法:

        private boolean onLoadSoundEffects() {int status;synchronized (mSoundEffectsLock) {if (!mSystemReady) {Log.w(TAG, "onLoadSoundEffects() called before boot complete");return false;}if (mSoundPool != null) {return true;}loadTouchSoundAssets();//根据XML文件初始化数组,如上所述//初始化SoundPoolmSoundPool = new SoundPool.Builder().setMaxStreams(NUM_SOUNDPOOL_CHANNELS).setAudioAttributes(new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build()).build();mSoundPoolCallBack = null;mSoundPoolListenerThread = new SoundPoolListenerThread();//这个线程以及下面的代码主要是去设置SoundPoolCallback,不求甚解mSoundPoolListenerThread.start();int attempts = 3;while ((mSoundPoolCallBack == null) && (attempts-- > 0)) {try {// Wait for mSoundPoolCallBack to be set by the other threadmSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);} catch (InterruptedException e) {Log.w(TAG, "Interrupted while waiting sound pool listener thread.");}}if (mSoundPoolCallBack == null) {Log.w(TAG, "onLoadSoundEffects() SoundPool listener or thread creation error");if (mSoundPoolLooper != null) {mSoundPoolLooper.quit();mSoundPoolLooper = null;}mSoundPoolListenerThread = null;mSoundPool.release();mSoundPool = null;return false;}/** poolId table: The value -1 in this table indicates that corresponding* file (same index in SOUND_EFFECT_FILES[] has not been loaded.* Once loaded, the value in poolId is the sample ID and the same* sample can be reused for another effect using the same file.*///创建一个和SOUND_EFFECT_FILES一样大的数组并将元素初始化为-1int[] poolId = new int[SOUND_EFFECT_FILES.size()];for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) {poolId[fileIdx] = -1;}/** Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded.* If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0:* this indicates we have a valid sample loaded for this effect.*/int numSamples = 0;for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {// Do not load sample if this effect uses the MediaPlayerif (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {continue;}//第一次走到这里时这个判断一定为真,因为poolId中所有元素都为-1if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {//getSoundEffectFilePath会根据SOUND_EFFECT_FILES中的内容补全出音频文件的具体路径String filePath = getSoundEffectFilePath(effect);//调用SoundPool.load方法,返回的sampleId被保存在SOUND_EFFECT_FILES_MAP和poolId中int sampleId = mSoundPool.load(filePath, 0);if (sampleId <= 0) {Log.w(TAG, "Soundpool could not load file: "+filePath);} else {SOUND_EFFECT_FILES_MAP[effect][1] = sampleId;poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId;numSamples++;}} else {SOUND_EFFECT_FILES_MAP[effect][1] =poolId[SOUND_EFFECT_FILES_MAP[effect][0]];}}// wait for all samples to be loadedif (numSamples > 0) {mSoundPoolCallBack.setSamples(poolId);attempts = 3;status = 1;while ((status == 1) && (attempts-- > 0)) {try {mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);status = mSoundPoolCallBack.status();} catch (InterruptedException e) {Log.w(TAG, "Interrupted while waiting sound pool callback.");}}} else {status = -1;}if (mSoundPoolLooper != null) {mSoundPoolLooper.quit();mSoundPoolLooper = null;}mSoundPoolListenerThread = null;if (status != 0) {Log.w(TAG,"onLoadSoundEffects(), Error "+status+ " while loading samples");for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) {SOUND_EFFECT_FILES_MAP[effect][1] = -1;}}mSoundPool.release();mSoundPool = null;}}return (status == 0);}private String getSoundEffectFilePath(int effectType) {String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH+ SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);if (!new File(filePath).isFile()) {filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH+ SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);}Log.d(TAG, "SoundEffectFilePath is : "+filePath);return filePath;}

代码中难懂的部分基本上都有注释,核心其实就是为SoundPool的load方法准备参数,其中有些数组嵌套的部分比较绕,但是只要把数组都写出来就一目了然了

onLoadSoundEffects基本上就是loadSoundEffects的全部内容,最后再来看一下onPlaySoundEffect的剩余部分

        private void onPlaySoundEffect(int effectType, int volume) {synchronized (mSoundEffectsLock) {onLoadSoundEffects();if (mSoundPool == null) {return;}float volFloat;// use default if volume is not specified by callerif (volume < 0) {volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);} else {volFloat = volume / 1000.0f;}if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {//调用SoundPool的play方法mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],volFloat, volFloat, 0, 0, 1.0f);Log.w(TAG, "Touch tone played");} 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);}}}}

其中值得注意的点其实就只有SoundPool的play方法,其中传入了音量大小和之前load返回的sampleId

二、替换原生按键音

摘要:替换原生按键音的主要思路是:在初始化的时候在相关数组中增加自己自定义的音频资源,为了达到这个目的需要在文件中增加一些代表自己文件资源的常量,具体在哪个文件中增加,其实完全可以在熟悉源码流程之后模仿源码来增加;之后在播放按键音的时候主动调用自己的按键音资源就可以了;最后当然别忘了把音频文件push到设备中去。

需要修改的文件如下:

/frameworks/base/media/java/android/media/AudioManager.java
需要增加自己的音频种类,起名为:FX_KEYPRESS_CUSTOM,并把最大音频数量修改为11

    /*** Invalid keypress sound* @see #playSoundEffect(int)*/public static final int FX_KEYPRESS_INVALID = 9;/*** @hide Custom sound* @see #playSoundEffect(int)*/public static final int FX_KEYPRESS_CUSTOM = 10;/*** @hide Number of sound effects*/public static final int NUM_SOUND_EFFECTS = 11;

需要注意的是自己增加的常量最好全部hide标记,这样可以免去执行make update-api指令,同时并不会影响使用,之后的修改都会遵循这一原则

/frameworks/base/core/java/android/view/SoundEffectConstants.java
同样需要增加一个常量:

    public static final int CLICK = 0;public static final int NAVIGATION_LEFT = 1;public static final int NAVIGATION_UP = 2;public static final int NAVIGATION_RIGHT = 3;public static final int NAVIGATION_DOWN = 4;/*** @hide Custom click sound*/public static final int CLICK_CUSTOM = 5;

/frameworks/base/core/res/res/xml/audio_assets.xml
在XML文件中增加一个自己的音频文件,注意id和之前在AudioManager.java中增加的常量一致,file和push到设备中的文件名保持一致

<audio_assets version="1.0"><group name="touch_sounds"><asset id="FX_KEY_CLICK" file="Effect_Tick.ogg"/><asset id="FX_FOCUS_NAVIGATION_UP" file="Effect_Tick.ogg"/><asset id="FX_FOCUS_NAVIGATION_DOWN" file="Effect_Tick.ogg"/><asset id="FX_FOCUS_NAVIGATION_LEFT" file="Effect_Tick.ogg"/><asset id="FX_FOCUS_NAVIGATION_RIGHT" file="Effect_Tick.ogg"/><asset id="FX_KEYPRESS_STANDARD" file="KeypressStandard.ogg"/><asset id="FX_KEYPRESS_SPACEBAR" file="KeypressSpacebar.ogg"/><asset id="FX_KEYPRESS_DELETE" file="KeypressDelete.ogg"/><asset id="FX_KEYPRESS_RETURN" file="KeypressReturn.ogg"/><asset id="FX_KEYPRESS_INVALID" file="KeypressInvalid.ogg"/><asset id="FX_KEYPRESS_CUSTOM" file="boom.ogg"/></group>
</audio_assets>

准备工作已经完成了,现在来修改一下调用流程,主动调用自己的按键音

/frameworks/base/core/java/android/view/View.java
调用playSoundEffect时传入之前增加的常量:

    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;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK_CUSTOM);//修改这里li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}

/frameworks/base/core/java/android/view/ViewRootImpl.java
View.java之后会调用到ViewRootImpl.java中,在switch/case中加入我们自己的情况:

    @Overridepublic void playSoundEffect(int effectId) {checkThread();Log.d(mTag, "playSoundEffect");try {final AudioManager audioManager = getAudioManager();switch (effectId) {case SoundEffectConstants.CLICK: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;//增加的case语句case SoundEffectConstants.CLICK_CUSTOM:audioManager.playSoundEffect(AudioManager.FX_KEYPRESS_CUSTOM);Log.d(mTag, "play my SoundEffect");return;default:throw new IllegalArgumentException("unknown effect id " + effectId +" not defined in " + SoundEffectConstants.class.getCanonicalName());}} catch (IllegalStateException e) {// Exception thrown by getAudioManager() when mView is nullLog.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);e.printStackTrace();}}

以上就是修改的全部文件了,实际上只有5个文件,比预想的要简单的多,这全都要归功于Android源码出色的设计模式使其在代码上高度解耦

别忘了把音频文件push到设备里面,否则会启动异常的哦!push的路径为:/system/media/audio/ui/

可能有些小伙伴对于为什么要修改上面的文件有一些疑问,这里附上播放按键音的UML时序图,只要熟悉调用流程,就明白了

三、调节按键音音量方法

其实在之前的讲解过程中已经说到了,在调用SoundPool.play的时候其实会传入左右声道的音量值,只要按图索骥找到之前是在哪里传入的音量就可以啦!其实是在playSoundEffectVolume方法传入的音量值,那么只要在这个方法的参数中传入你想要的值就行了。

Android9 framework 按键音调用流程及自定义按键音(替换原生按键音)和调节按键音音量的方法相关推荐

  1. Android system server之WindowManagerService按键消息传播流程

    主要涉及的文件有: WindowManagerService.java   frameworks\base\services\java\com\android\server\ PhoneWindow. ...

  2. stm32 工业按键检测_「正点原子STM32Mini板资料连载」第七章 按键输入实验

    1)实验平台:正点原子STM32mini开发板 2)摘自<正点原子STM32 不完全手册(HAL 库版)>关注官方微信号公众号,获取更多资料:正点原子 第七章 按键输入实验 上一章,我们介 ...

  3. android 按钮 的亮度,HTC手机如何调节按键灯亮度和按键灯开关?HTC通用按键灯开关及亮度调节教程...

    HTC ONE M8把工业设计和设计美学有机的结合在了一起,但是用户不难发现HTC为了响应谷歌用虚拟按键替换了了实体按键.那么,我们的问题来了.那些过去使用实体按键的HTC机型该如何关掉在晚上晃眼的按 ...

  4. php 获取域名前缀,蓝奏批量自定义域名前缀替换php源码

    蓝奏批量自定义域名前缀替换php源码,最近蓝奏云推出了一个自定义域名前缀的功能,譬如https://www.lanzous.com/ibd9qnc这个链接,可以将www替换为任意的前缀,比方说cr17 ...

  5. 我的世界服务器如何修改武器,我的世界:自定义装备属性,让你自己动手来“调节”装备属性!...

    原标题:我的世界:自定义装备属性,让你自己动手来"调节"装备属性! 很多小伙伴们都玩过RPG的地图吧,有没有见过那些非常厉害的RPG的物品呢,像什么武器啦,装备啦什么的,都是非常厉 ...

  6. android 虚拟按键源码流程分析

    android 虚拟按键流程分析 今天来说说android 的虚拟按键的源码流程.大家都知道,android 系统的状态栏,虚拟按键,下拉菜单,以及通知显示,keyguard 锁屏都是在framewo ...

  7. 安卓蓝牙键盘按键映射_双层按键功能自定义:魔蛋68蓝牙双模机械键盘体验

    魔蛋68机械键盘最早在2015年面世,而该款机型,至今仍然在售.当然,期间曾经历过数次升级调整,但始终不变的是这副金属上盖悬浮外观以及68键配列.尤其是后者,相同按键布局量产机械键盘并没太多选择,而魔 ...

  8. 图解classloader加载class的流程及自定义ClassLoader

    http://longdick.iteye.com/blog/442213 java应用环境中不同的class分别由不同的ClassLoader负责加载. 一个jvm中默认的classloader有B ...

  9. Dynamic CRM 2013学习笔记(四十三)流程6 - 自定义流程活动

    当我们在流程里添加步骤时,有一些默认的步骤,像创建.更新.发邮件等,但如果你想加一个里面没有的步骤,比如发SMS消息,或者调用一个外部的web service,怎么办?这时就只能自定义一个流程活动了. ...

最新文章

  1. 数据结构和算法:(1)数据结构的基本知识
  2. mysql注释符号按键,Shell 注释
  3. mysql的repeat()函数
  4. 做一个类似登录的循环
  5. 修改MongoDB密码
  6. 数据结构上机实践第11周项目2 - 操作用邻接表存储的图
  7. OpenCV :(-5:Bad argument) Matrix operand is an empty matrix. in function ‘checkOperandsExist‘
  8. 体验汉印T260标签打印机,让分类管理更简单
  9. Win11任务栏不显示时间怎么办?Win11任务栏不显示时间的解决方法
  10. 关于破解excel表格密码
  11. 近几年网络营销成功案例精选
  12. 机器学习案例-信用卡诈骗识别。
  13. 关键词抓取规则,关键词标题SEO技巧
  14. 苹果A14和高通骁龙888性能对比,A13都笑了
  15. 如何申请小程序与小程序支付
  16. 针对文件流转Base64的操作
  17. js中判断两个对象是否相等。
  18. 1-Java的诞生和发展
  19. :before和::before是什么区别
  20. attiny13a程序实例_关于ATtiny13A的程序

热门文章

  1. 深入浅出TCP/UDP 原理-UDP篇(2)及完整MATLAB实现UDP通信
  2. 液压器大数据分析 需求调研计划
  3. springmvc拦截器 绝对路径
  4. 后向投影算法(BPA)-SAR成像算法系列(二)
  5. 7-1 换硬币 将一笔零钱换成5分、2分和1分的硬币,要求每种硬币至少有一枚,有几种不同的换法?
  6. Unity实战问题--导入视频格式问题(QuickTime player) 转
  7. 一款文件加密器,分享给大家
  8. 工具软件 PYUV打开raw图片
  9. 数据钻取,详细数据一览无遗!
  10. 【关于四足机器人那些事】腿部运动学建模(三维)