目前Android的实现是:有来电时,音乐声音直接停止,铃声直接直接使用设置的铃声音量进行铃声播放。

Android 4.3实现类似iOS在音乐播放过程中如果有来电则音乐声音渐小铃声渐大的效果。

如果要实现这个效果,首先要搞清楚两大问题;

1、来电时的代码主要实现流程。

2、主流音乐播放器在播放过程中,如果有来电,到底在收到了什么事件后将音乐暂停了?

一:来电时的代码主要实现流程

我不是第一研究来电代码的人,网上已经有高手对这个流程剖析过,不是不完全符合我的要求,我参考过的比较有价值的是如下两个文档:

Android来电时停止音乐播放的流程

Android源码分析:Telephony部分–phone进程

有参考价值,但都分析很比较粗略,只能自己再一步一步跟源码进一步了解。

因为我做的事情主要是有来电时,修改铃音的效果,所以不用从头跟进,从响铃通知到达Phone.apk中分析起即可,更细可以参考下上面的两个链接。

分析之前,还是有必要对Phone整体的初始化流程有个基本认识,不然后面跟到沟里去。

Phone.apk 的AndroidManifest.xml中的application的说明:

  1. <application android:name="PhoneApp"
  2. android:persistent="true"
  3. android:label="@string/phoneAppLabel"
  4. android:icon="@mipmap/ic_launcher_phone">

那再看看PhoneApp的实现:

  1. /**
  2. * Top-level Application class for the Phone app.
  3. */
  4. public class PhoneApp extends Application {
  5. PhoneGlobals mPhoneGlobals;
  6. public PhoneApp() {
  7. }
  8. @Override
  9. public void onCreate() {
  10. if (UserHandle.myUserId() == 0) {
  11. // We are running as the primary user, so should bring up the
  12. // global phone state.
  13. mPhoneGlobals = new PhoneGlobals(this);
  14. mPhoneGlobals.onCreate();
  15. }
  16. }
  17. @Override
  18. public void onConfigurationChanged(Configuration newConfig) {
  19. if (mPhoneGlobals != null) {
  20. mPhoneGlobals.onConfigurationChanged(newConfig);
  21. }
  22. super.onConfigurationChanged(newConfig);
  23. }

从源码来看,这个类非常的简单,主要就是对 mPhoneGlobals 属性进行了创建和初始化。再来分析 PhoneGlobals 是如何初始化的:

  1. public void PhoneGlobals.onCreate() {
  2. ...
  3. if (phone == null) {
  4. // Initialize the telephony framework
  5. PhoneFactory.makeDefaultPhones(this);
  6. // Get the default phone
  7. phone = PhoneFactory.getDefaultPhone();
  8. // Start TelephonyDebugService After the default phone is created.
  9. Intent intent = new Intent(this, TelephonyDebugService.class);
  10. startService(intent);
  11. mCM = CallManager.getInstance();
  12. mCM.registerPhone(phone);
  13. // Create the NotificationMgr singleton, which is used to display
  14. // status bar icons and control other status bar behavior.
  15. notificationMgr = NotificationMgr.init(this);
  16. phoneMgr = PhoneInterfaceManager.init(this, phone);
  17. mHandler.sendEmptyMessage(EVENT_START_SIP_SERVICE);
  18. int phoneType = phone.getPhoneType();
  19. if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
  20. // Create an instance of CdmaPhoneCallState and initialize it to IDLE
  21. cdmaPhoneCallState = new CdmaPhoneCallState();
  22. cdmaPhoneCallState.CdmaPhoneCallStateInit();
  23. }
  24. ...
  25. ringer = Ringer.init(this);
  26. ...
  27. notifier = CallNotifier.init(this, phone, ringer, new CallLogAsync());
  28. ...
  29. }
  30. ...
  31. }

PhonePhoneGlobals.onCreate()  中干了很多事情,其中我列出的内容,都是我个人觉得比较重要的部分,建议重点看一下,后面会用得到。

PhoneFactory.makeDefaultPhones(this) 和 phone = PhoneFactory.getDefaultPhone() 这两个函数调用,建议也跟进去重点看一下,这里面做了比较重要的事情,

底层来电事件就是通过类似注册表注册机制做好一系列地注册之后,后面有不同事件过来后,将相应的消息分发特定的对象去处理。

我修改了Phone的源码,将日志全部放开,然后将重新编译得到的 Phone.apk 更新到手机中,真实地拨打了一个电话,

日志量比较大,只列出开头的一小部分,具体日志如下:

  1. 10-10 21:20:18.862: D/CallNotifier(814): RING before NEW_RING, skipping
  2. 10-10 21:20:18.862: D/InCallScreen(814): Handler: handling message { what=123 when=0 obj=android.os.AsyncResult@418f38f8 } while not in foreground
  3. 10-10 21:20:18.862: D/InCallScreen(814): onIncomingRing()...
  4. 10-10 21:20:20.834: D/CallNotifier(814): PHONE_ENHANCED_VP_OFF...
  5. 10-10 21:20:20.844: D/CallNotifier(814): RINGING... (new)
  6. 10-10 21:20:20.844: D/CallNotifier(814): onNewRingingConnection(): state = RINGING, conn = {  incoming: true state: INCOMING post dial state: NOT_STARTED }
  7. 10-10 21:20:20.844: D/CallNotifier(814): Incoming number is: 02556781234
  8. 10-10 21:20:20.844: V/BlacklistProvider(814): Query uri=content://blacklist/bynumber/02556781234, match=2
  9. 10-10 21:20:20.864: D/CallNotifier(814): stopSignalInfoTone: Stopping SignalInfo tone player
  10. 10-10 21:20:20.864: D/CallNotifier(814): - connection is ringing!  state = INCOMING
  11. 10-10 21:20:20.864: D/CallNotifier(814): Holding wake lock on new incoming connection.
  12. 10-10 21:20:20.864: D/PhoneApp(814): requestWakeState(PARTIAL)...
  13. 10-10 21:20:20.864: D/PhoneUtils(814): PhoneUtils.startGetCallerInfo: new query for phone number...
  14. ...

从上面的日志可以看出,当有来电时,其实是 PHONE_NEW_RINGING_CONNECTION 这个事件交给了Phoe应用来处理了。

底层的流程大致如下,更详细的参见《Android来电时停止音乐播放的流程》:

        1).    RIL在接收到请求的时候会向GsmCallTracker广播消息,而GsmCallTracker在接收到该消息的时候会继续
                向上层的CallManager广播
        2).    CallManager在这个只充当了一个转播者的角色,它会继续将消息传播给CallNotifier
        3).    而CallNotifier接收到消息后会判断来电是否需要查询,不查询则会直接设置声音模式(包含停止音乐播放并
                开始响铃)并显示来电界面等待用户的下一步操作; 若需要查询则会在查询接收后执行此部分过程 

从代码层面上,这个是如何体现的呢?

1、RIL怎么将消息传递给 GsmCallTracker 的,这个没有研究,跳过。

2、GsmCallTracker如何将消息向上层传播的?来看看代码:GsmCallTracker这个类本身是继承自Handler这个类的,看看handleMessage (Message msg)实现:

  1. handleMessage (Message msg) {
  2. AsyncResult ar;
  3. switch (msg.what) {
  4. case EVENT_POLL_CALLS_RESULT:
  5. ar = (AsyncResult)msg.obj;
  6. if (msg == lastRelevantPoll) {
  7. if (DBG_POLL) log(
  8. "handle EVENT_POLL_CALL_RESULT: set needsPoll=F");
  9. needsPoll = false;
  10. lastRelevantPoll = null;
  11. handlePollCalls((AsyncResult)msg.obj);
  12. }
  13. break;
  14. ...
  15. }
  16. }

再看看handlePollCalls()的实现:

  1. protected synchronized void
  2. handlePollCalls(AsyncResult ar) {
  3. ...
  4. if (newRinging != null) {
  5. phone.notifyNewRingingConnection(newRinging);
  6. }
  7. ...
  8. updatePhoneState();
  9. ...
  10. }

重点关注有来电相关的代码, GSMPhone.notifyNewRingingConnection(newRinging); -->  PhoneBase.notifyNewRingingConnectionP()

--> PhoneBase.mNewRingingConnectionRegistrants.notifyRegistrants(ar) --> ...
一路跟下去,到 Registrant.internalNotifyRegistrant(),这个是这个 h 到底对应的是哪个Handler呢?

  1. /*package*/ void
  2. internalNotifyRegistrant (Object result, Throwable exception)
  3. {
  4. Handler h = getHandler();
  5. if (h == null) {
  6. clear();
  7. } else {
  8. Message msg = Message.obtain();
  9. msg.what = what;
  10. msg.obj = new AsyncResult(userObj, result, exception);
  11. h.sendMessage(msg);
  12. }
  13. }

我们在前面看的初始化相关的代码的作用就体现出来了,PhoneBase.mNewRingingConnectionRegistrants这个列表中的内容是何时放进去的呢?

  1. /** Private constructor; @see init() */
  2. private CallNotifier(PhoneGlobals app, Phone phone, Ringer ringer, CallLogAsync callLog) {
  3. mApplication = app;
  4. mCM = app.mCM;
  5. mCallLog = callLog;
  6. mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE);
  7. registerForNotifications();
  8. ...
  1. private void registerForNotifications() {
  2. mCM.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);
  3. ...

mCM就是CallManager对象,CallNotifier在初步化时将自己与PHONE_NEW_RINGING_CONNECTION事件的关系注册到了CallManager的mNewRingingConnectionRegistrants对象中。

  1. /**
  2. * Notifies when a new ringing or waiting connection has appeared.<p>
  3. *
  4. *  Messages received from this:
  5. *  Message.obj will be an AsyncResult
  6. *  AsyncResult.userObj = obj
  7. *  AsyncResult.result = a Connection. <p>
  8. *  Please check Connection.isRinging() to make sure the Connection
  9. *  has not dropped since this message was posted.
  10. *  If Connection.isRinging() is true, then
  11. *   Connection.getCall() == Phone.getRingingCall()
  12. */
  13. public void registerForNewRingingConnection(Handler h, int what, Object obj){
  14. mNewRingingConnectionRegistrants.addUnique(h, what, obj);
  15. }

CallNotifier也是继承了Handler的,在上面的 internalNotifyRegistrant()
中,最终也是将消息发送给 CallNotifier 对象去处理的,CallNotifier 的 handleMessage()
函数就会被间接地调用了。
下面进入CallNotifier 的 handleMessage(),看看它的实现:

  1. @Override
  2. public void handleMessage(Message msg) {
  3. switch (msg.what) {
  4. case PHONE_NEW_RINGING_CONNECTION:
  5. log("RINGING... (new)");
  6. mSilentRingerRequested = false;
  7. ((AsyncResult) msg.obj);
  8. break;
  9. ...

看看这里输出的日志,在上面我列出的日志中是有输出的:  "RINGING... (new)"。再跟到 onNewRingingConnection() 看看:

  1. /**
  2. * Handles a "new ringing connection" event from the telephony layer.
  3. */
  4. private void onNewRingingConnection(AsyncResult r) {
  5. Connection c = (Connection) r.result;
  6. log("onNewRingingConnection(): state = " + mCM.getState() + ", conn = { " + c + " }");
  7. Call ringing = c.getCall();
  8. Phone phone = ringing.getPhone();
  9. // Check for a few cases where we totally ignore incoming calls.
  10. if (ignoreAllIncomingCalls(phone)) {
  11. // Immediately reject the call, without even indicating to the user
  12. // that an incoming call occurred.  (This will generally send the
  13. // caller straight to voicemail, just as if we *had* shown the
  14. // incoming-call UI and the user had declined the call.)
  15. PhoneUtils.hangupRingingCall(ringing);
  16. return;
  17. }
  18. ...
  19. // - don't ring for call waiting connections
  20. // - do this before showing the incoming call panel
  21. if (PhoneUtils.isRealIncomingCall(state)) {
  22. startIncomingCallQuery(c);
  23. }
  24. }

主要的逻辑就是判断基于一定的规则判断是否自动拦截此呼叫,如果不拦截,则会向下走,调用到 startIncomingCallQuery() 函数。

这个函数,干的事情也比较简单,就是基于号码来查询联系人详情啥的,如果获取到联系人信息,则根据这个结果判断是使用默认铃声,还是用户给其设置的特定铃声。

  1. /**
  2. * Helper method to manage the start of incoming call queries
  3. */
  4. private void startIncomingCallQuery(Connection c) {
  5. ...
  6. if (shouldStartQuery) {
  7. // Reset the ringtone to the default first.
  8. mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI);
  9. // query the callerinfo to try to get the ringer.
  10. PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo(
  11. mApplication, c, this, this);
  12. // if this has already been queried then just ring, otherwise
  13. // we wait for the alloted time before ringing.
  14. if (cit.isFinal) {
  15. if (VDBG) log("- CallerInfo already up to date, using available data");
  16. onQueryComplete(0, this, cit.currentInfo);
  17. } else {
  18. if (VDBG) log("- Starting query, posting timeout message.");
  19. // Phone number (via getAddress()) is stored in the message to remember which
  20. // number is actually used for the look up.
  21. sendMessageDelayed(
  22. Message.obtain(this, RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT, c.getAddress()),
  23. RINGTONE_QUERY_WAIT_TIME);
  24. }
  25. // The call to showIncomingCall() will happen after the
  26. // queries are complete (or time out).
  27. } ...
  28. }

这里面有一点细节要说明一下,PhoneUtils.startGetCallerInfo() 这个调用之后,如果成功,则会再回调到 CallNotifier.onQueryComplete();

为了防止PhoneUtils.startGetCallerInfo()出现异常长时间不回调,在else这个分支中,还插入了一个RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT
这样一个消息,在500ms后,如果CallNotifier.onQueryComplete()没有被回调,则此消息会被触发。不管有没有超
时,onCustomRingQueryComplete()
都会被调用到。

具体是使用到了Handler的机制,Handler的原理说明可以参见我的这个blog:《深入理解Android消息处理系统——Looper、Handler、Thread》。

再看看 onCustomRingQueryComplete() 的实现:
  1. /**
  2. * Performs the final steps of the onNewRingingConnection sequence:
  3. * starts the ringer, and brings up the "incoming call" UI.
  4. *
  5. * Normally, this is called when the CallerInfo query completes (see
  6. * onQueryComplete()).  In this case, onQueryComplete() has already
  7. * configured the Ringer object to use the custom ringtone (if there
  8. * is one) for this caller.  So we just tell the Ringer to start, and
  9. * proceed to the InCallScreen.
  10. *
  11. * But this method can *also* be called if the
  12. * RINGTONE_QUERY_WAIT_TIME timeout expires, which means that the
  13. * CallerInfo query is taking too long.  In that case, we log a
  14. * warning but otherwise we behave the same as in the normal case.
  15. * (We still tell the Ringer to start, but it's going to use the
  16. * default ringtone.)
  17. */
  18. private void onCustomRingQueryComplete() {
  19. ...
  20. // Ring, either with the queried ringtone or default one.
  21. if (VDBG) log("RINGING... (onCustomRingQueryComplete)");
  22. mRinger.ring();
  23. // ...and display the incoming call to the user:
  24. if (DBG) log("- showing incoming call (custom ring query complete)...");
  25. showIncomingCall();
  26. }

从注释上就可以看出,这个是 onNewRingingConnection 的事件处理序列的最后一步,主要干两件事:

    1、触发铃声的播放;
    2、显示来电界面;

第一个是我更想关心的,再看看这个干了什么,说不定就是我们要修改的地方:

进入到Ringer.ring()的实现看看,如果铃声音量值不是0,就发PLAY_RING_ONCE消息去播放铃声:

  1. void ring() {
  2. if (DBG) log("ring()...");
  3. synchronized (this) {
  4. ...
  5. AudioManager audioManager =
  6. (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
  7. if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
  8. if (DBG) log("skipping ring because volume is zero");
  9. return;
  10. }
  11. makeLooper();
  12. if (mFirstRingEventTime < 0) {
  13. mFirstRingEventTime = SystemClock.elapsedRealtime();
  14. mRingHandler.sendEmptyMessage(PLAY_RING_ONCE);
  15. } ...
  16. }
  17. }

makeLooper()中有对 mRingHandler有初始化:

  1. private void makeLooper() {
  2. if (mRingThread == null) {
  3. mRingThread = new Worker("ringer");
  4. mRingHandler = new Handler(mRingThread.getLooper()) {
  5. @Override
  6. public void handleMessage(Message msg) {
  7. Ringtone r = null;
  8. switch (msg.what) {
  9. case PLAY_RING_ONCE:
  10. if (DBG) log("mRingHandler: PLAY_RING_ONCE...");
  11. if (mRingtone == null && !hasMessages(STOP_RING)) {
  12. // create the ringtone with the uri
  13. if (DBG) log("creating ringtone: " + mCustomRingtoneUri);
  14. r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
  15. synchronized (Ringer.this) {
  16. if (!hasMessages(STOP_RING)) {
  17. mRingtone = r;
  18. }
  19. }
  20. }
  21. r = mRingtone;
  22. if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
  23. PhoneUtils.setAudioMode();
  24. r.play();
  25. synchronized (Ringer.this) {
  26. if (mFirstRingStartTime < 0) {
  27. mFirstRingStartTime = SystemClock.elapsedRealtime();
  28. }
  29. }
  30. }
  31. break;
  32. ...
  33. }
  34. }
  35. };
  36. }
  37. }

会初始化出一个Ringtone对象,通过这个对象来播放铃声,这个Ringtone播放铃声其实还有点绕的,最终是通过Binder机制使用"audio"服务中的Ringtone对象中的mLocalPlayer属性,即MediaPlayer的实例来播放铃声的。怎么实现的,这里就不说了,代码太多了,而且还涉及到Binder机制,如果有疑问,可以单独找我。

总算找到开始播放铃声的代码了,在这附近加一些逻辑来控制铃声音量、和音乐音量的代码就可以了。

通过 r.play() 附近加上如下逻辑:

  1. mHandler.sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200);
  2. mHandler.sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);

makeLooper()中再加上如下代码:

  1. if (mHandler == null) {
  2. mHandler = new Handler() {
  3. @Override
  4. public void handleMessage(Message msg) {
  5. switch (msg.what) {
  6. case INCREASE_RING_VOLUME:
  7. int ringerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
  8. if (mRingerVolumeSetting > 0 && ringerVolume < mRingerVolumeSetting) {
  9. ringerVolume++;
  10. mAudioManager.setStreamVolume(AudioManager.STREAM_RING, ringerVolume, 0);
  11. sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200);
  12. }
  13. break;
  14. case DECREASE_MUSIC_VOLUME:
  15. int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
  16. if (musicVolume > 0) {
  17. musicVolume--;
  18. mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, musicVolume, 0);
  19. sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);
  20. }
  21. break;
  22. }
  23. }
  24. };
  25. }

当然,你还要考虑一些细节,比如Music是否正在播放,铃声或音乐的音量大小是否是0,或最大等。

AudioManager中的一些说明,可以参见《Android如何判断当前手机是否正在播放音乐,并获取到正在播放的音乐的信息》。

当我修改完代码,并怀着十分期待的心情将Phone.apk替换原有的apk后,拨打被叫有来电时,正在播放的音乐一下就停止了,铃音是渐强的,哪里出了问题?

分析清楚这个问题花的时间比之前还要长,有空再写下面的内容吧。

Android 4.3实现类似iOS在音乐播放过程中如果有来电则音乐声音渐小铃声渐大的效果的更多相关文章

  1. iOS下KVO使用过程中的陷阱 (转发)

    iOS下KVO使用过程中的陷阱   KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应.网上广为流传普及的一个例子是利用KV ...

  2. Swift版音乐播放器(简化版),swift音乐播放器

    这几天闲着也是闲着,学习一下Swift的,于是到开源社区Download了个OC版的音乐播放器,练练手,在这里发扬开源精神, 希望对大家有帮助! 这个DEMO里,使用到了 AudioPlayer(对音 ...

  3. 016 Android 图片选择器(在选中和未选中的过程中,切换展示图片)

    1.目标效果 在选中和未选中的过程中,切换展示图片 2.实现方法 (1)在app--->res--->drawable 右击drawable文件夹右键,new ---->drawab ...

  4. iOS - 如何自动播放H5中的音频

    场景:iOS端设备,App页面跳转到H5产品介绍,背景音乐无法播放.(为什么不能自动播放,因该是iPhone人性化设定吧~) 加载H5用UIWebView空间: 代码: CGRect rect = s ...

  5. 【原】iOS下KVO使用过程中的陷阱

    KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应.网上广为流传普及的一个例子是利用KVO检测股票价格的变动,例如这里.这个 ...

  6. Android Notification实现推送消息过程中接受到消息端有声音及震动及亮屏提示

    在Android Notification状态栏通知一文中,简单实现了消息的推送效果,这里就接着上文说一下,当用户接受到消息时的提示效果 // 5-增加震动及声音及亮屏 notification.de ...

  7. 【Android】11.3 屏幕旋转和场景变换过程中GridView的呈现

    分类:C#.Android.VS2015: 创建日期:2016-02-21 一.简介 实际上,对于布局文件中的View来说,大多数情况下,Android都会自动保存这些状态,并不需要我们都去处理它.这 ...

  8. 用Vue来实现音乐播放器(二十三):音乐列表

    当我们将音乐列表往上滑的时候   我们上面的歌手图片部分也会变小 当我们将音乐列表向下拉的时候   我们的图片会放大 当我们将音乐列表向上滑的时候   我们的图片有一个高斯模糊的效果 并且随着我们的列 ...

  9. iOS上传应用过程中出现的错误"images contain alpha channels or transparencies"以及解决方案

    如何取消图片透明度  本文永久地址为 http://www.cnblogs.com/ChenYilong/p/3989954.html,转载请注明出处. 当你试图通过<预览>进行" ...

随机推荐

  1. vim自定义配置之代码折叠

    vimConfig/plugin/codeFold-setting.vim "--fold setting-- set foldmethod=syntax " 用语法高亮来定义折叠 ...

  2. struts2学习(2)struts2核心知识

    一.Struts2 get/set 自动获取/设置数据 根据上一章.中的源码继续. HelloWorldAction.java中private String name,自动获取/设置name: pac ...

  3. JVM体系结构之七:持久代、元空间(Metaspace) 常量池==了解String类的intern()方法、常量池介绍、常量池从Perm-->Heap

    一.intern()定义及使用 相信绝大多数的人不会去用String类的intern方法,打开String类的源码发现这是一个本地方法,定义如下: public native String inter ...

  4. 【ZZ】MySQL 索引优化全攻略 | 菜鸟教程

    MySQL 索引优化全攻略 http://www.runoob.com/w3cnote/mysql-index.html

  5. 第八章(三)基于Listcheck适配器的访问控

    denier适配器访问控制比较死板.Listchecker的适配器更加灵活. 定义handler: apiVersion: config.istio.io/v1alpha2 kind: listche ...

  6. 简单神经网络TensorFlow实现

    学习TensorFlow笔记 import tensorflow as tf #定义变量 #Variable 定义张量及shape w1= tf.Variable(tf.random_normal([ ...

  7. 【DataGuard】部署Data Guard相关参数详解 (转载)

    原文地址:[DataGuard]部署Data Guard相关参数详解 作者:secooler    有关物理Data Guard部署参考<[DataGuard]同一台主机实现物理Data Gua ...

  8. 给iOS开发新手送点福利,简述UIAlertView的属性和用法

    UIAlertView 1.Title 获取或设置UIAlertView上的标题. 2.Message 获取或设置UIAlertView上的消息 UIAlertView *alertView = [[ ...

  9. 安装Android studio出现'tools.jar' seems to be not in Android Studio classpath......的解决方法

    安装Android studio出现'tools.jar' seems to be not in Android Studio classpath......的解决方法 原创 2015年07月31日 ...

  10. delphi使用 DockForm DesignEditors F2613 Unit 'DockForm' not found

    DockForm [dcc32 Fatal Error] ToolsAPI.pas(18): F2613 Unit 'DockForm' not found. 这样解决了XE7. http://doc ...