Android窗口系統第四篇---Activity動畫的設置過程

無論是系統中窗口的動畫,還是應用中某一個View的動畫,它們的原理都是一樣的。當一個窗口打開的時候,為了看起來更緩和一點,系統都會給每一個Activity窗口添加一個動畫,關于動畫的部分,我所想寫的有四點。第一、動畫有哪些類型;第二動畫是怎么設置的,由于窗口動畫和過度動畫(Activity窗口動畫)是不一樣的,就需要分開講,在小米手機上,可以去開發者選項中將窗口動畫的播放速度降慢5倍或者10倍,可以清楚的看到動畫的過程。第三,動畫設置完成之后,怎么觸發垂直刷新信號一幀幀顯示的,由于一個窗口可能存在多個動畫,比如轉屏動畫、過度動畫、窗口動畫,自身動畫等等,最終交給SurfaceFlinger繪制顯示的時候,需要合成為一個動畫,所以在談一下動畫的合成;第四,簡單總結應用動畫,用一個貝塞爾曲線繪制直播間點贊效果的例子講解一下,總結而言,系統中的動畫和應用中的動畫原理是一樣的,這篇文章是站在系統的角度上,搞清楚動畫的原理,本文基于Android7.0源碼。

一、動畫類型

在Apptransition.java中定義了很多動畫的類型,每個類型以一個int值來表示。

動畫類型 含義
TRANSIT_UNSET -1 初始值,尚未設定
TRANSIT_NONE 0 沒有動畫
TRANSIT_ACTIVITY_OPEN 6 在同一task中在最頂端打開一個窗口
TRANSIT_ACTIVITY_CLOSE 7 關閉當前活動窗口,恢復同一個task中的上一個窗口
TRANSIT_TASK_OPEN 8 新建任務并創建窗口
TRANSIT_TASK_CLOSE 9 關閉當前活動窗口,回到上一個任務
TRANSIT_TASK_TO_FRONT 10 將任務移至最頂
TRANSIT_TASK_TO_BACK 11 將當前任務移至最末
TRANSIT_WALLPAPER_CLOSE 12 關閉到無墻紙的應用
TRANSIT_WALLPAPER_OPEN 13 啟動墻紙應用
TRANSIT_WALLPAPER_INTRA_OPEN 14 有墻紙打開
TRANSIT_WALLPAPER_INTRA_CLOSE 15 有墻紙關閉

默認是沒有動畫,即類型是TRANSIT_UNSET,拿Activity的啟動來舉例子,比如當一個Activity打開的時候,那么系統就會設置一個TRANSIT_ACTIVITY_OPEN的動畫,如果你startActivity組件的時候,Intent對象帶有FLAG_ACTIVITY_NO_ANIMATION這樣的flag,那么系統就會給你設置一個TRANSIT_NONE,表示沒有動畫,不需要動畫,如果你指定了lauchMode,跨Task棧新起了一個Actiivty,那么就會設置一個TRANSIT_TASK_OPEN類型,表示新建任務并創建窗口時候要用的動畫,同理當Activity的關閉的時候,也類似,總之根據不同的case,設置不同的類型,后面根據這個設置好的類型,加載不同的動畫。

二、動畫設置

了解了動畫類型了,我們看一下Activity切換的時候,動畫是怎么設置的,先簡單看一下Activity的切換。

1、Activiy切換
Activiy切換

什么是Activity的切換呢?
  前一個Activity從resume狀態變成pause狀態,后一個Activity進入到resume狀態,將前一個resume狀態的窗口設置成不可見,后一個窗口設置成可見。

切換的步驟

  • ActivityStack類的成員函數startActivityLocked首先會給正在啟動的Activity組件準備一個切換操作,這里所說的切換操作,你可以理解成前面設置的動畫類型。
  • 接著再調用其它的成員函數來通知前一個激活的Activity組件進入到Paused狀態。
  • 等到前一個激活的Activity組件進入到Paused狀態之后,ActivityManagerService服務就會檢查用來運行正在啟動的Activity組件的進程啟動起來了沒有。如果這個進程還沒有啟動,那么ActivityManagerService服務就會將該進程啟動起來,然后再調用ActivityStack類的成員函數realStartActivityLocked來將正在啟動的Activity組件加載起來,并且將它的狀態設置為Resumed。這里面具體又分成兩個小點,一是setAppVisibility 、二是通知lauch Activity。
  • 最后通知WindowManagerService服務執行前面所準備的切換操作

首先梳理一下prepareAppTransition方法。設置什么樣的切換操作,其實由Activity的行為決定。

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

   @Override
   public void prepareAppTransition(int transit, boolean alwaysKeepCurrent) {
       if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
               "prepareAppTransition()")) {
           throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
       }
       synchronized(mWindowMap) {
           boolean prepared = mAppTransition.prepareAppTransitionLocked(
                   transit, alwaysKeepCurrent);
           //prepared為ture,說明已經被成功設置了切換操作,但是當前凍屏、熄屏、Display沒有準備好的情況下,
           //設置mSkipAppTransitionAnimation等于true,表示要跳過這切換操作對應動畫的執行。
           if (prepared && okToDisplay()) {
               mSkipAppTransitionAnimation = false;
           }
       }
   }
  frameworks/base/services/core/java/com/android/server/wm/AppTransition.java

   boolean prepareAppTransitionLocked(int transit, boolean alwaysKeepCurrent) {
      .....
       //isTransitionSet()表示已經設置了切換操作類型
       if (!isTransitionSet() || mNextAppTransition == TRANSIT_NONE) {
           setAppTransition(transit);
       } else if (!alwaysKeepCurrent) {
           //alwaysKeepCurrent若等于true,表示要維持上次設置的切換類型,本次新設置的不能覆蓋它
           if (transit == TRANSIT_TASK_OPEN && isTransitionEqual(TRANSIT_TASK_CLOSE)) {
               // Opening a new task always supersedes a close for the anim.
               setAppTransition(transit);
           } else if (transit == TRANSIT_ACTIVITY_OPEN
                   && isTransitionEqual(TRANSIT_ACTIVITY_CLOSE)) {
               // Opening a new activity always supersedes a close for the anim.
               setAppTransition(transit);
           }
       }
       boolean prepared = prepare();
       if (isTransitionSet()) {
           //發送一個5秒的超時消息給WMS運行的線程(android.display線程),表示要在5秒時間類完成設置的切換操作。
           mService.mH.removeMessages(H.APP_TRANSITION_TIMEOUT);
           mService.mH.sendEmptyMessageDelayed(H.APP_TRANSITION_TIMEOUT, APP_TRANSITION_TIMEOUT_MS);
       }
       return prepared;
   }

如果transit == TRANSIT_TASK_OPEN 并且isTransitionEqual(TRANSIT_TASK_CLOSE)返回true,表示上次(之前)給Activity設置的切換操作是TRANSIT_TASK_CLOSE,那么可以調用setAppTransition,因為打開的動畫要比關閉的動畫優先級要高。

如果transit == TRANSIT_ACTIVITY_OPEN 并且isTransitionEqual(TRANSIT_ACTIVITY_CLOSE)返回true,表示上次(之前)給Activity設置的切換操作是TRANSIT_ACTIVITY_CLOSE,那么可以調用setAppTransition,因為打開的動畫要比關閉的動畫優先級要高。

  frameworks/base/services/core/java/com/android/server/wm/AppTransition.java
   private void setAppTransition(int transit) {
       mNextAppTransition = transit;
       setLastAppTransition(TRANSIT_UNSET, null, null);
   }

setAppTransition執行過后,并且前一個激活的Activity組件進入到Paused狀態了,并且客戶端進程已經啟動了,這個時候ActivityManagerService服務就會調用ActivityStack類的成員函數realStartActivityLocked來將正在啟動的Activity組件加載起來,并且將它的狀態設置為Resumed,首先看一下setAppVisibility,將窗口可見性的設置。

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

 @Override
   public void setAppVisibility(IBinder token, boolean visible) {
       .....
       AppWindowToken wtoken;

       synchronized(mWindowMap) {
          //通過ActivityRecord:Token找到AppWindowToken,即找到這個token對應的Activity窗口
           wtoken = findAppWindowToken(token);
           if (wtoken == null) {
               Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: " + token);
               return;
           }
           .....
            //mOpeningApps是WMS的成員,里面存放所有打開的窗口的AppWindowToken,首先移除,后面根據visible添加
           mOpeningApps.remove(wtoken);
             //mClosingApps是WMS的成員,里面存放所有關閉的窗口的AppWindowToken,首先移除,后面根據visible添加
           mClosingApps.remove(wtoken);
           //表示等待著去顯示
           wtoken.waitingToShow = false;
           wtoken.hiddenRequested = !visible;
           if (!visible) {
               // If the app is dead while it was visible, we kept its dead window on screen.
               // Now that the app is going invisible, we can remove it. It will be restarted
               // if made visible again.
               wtoken.removeAllDeadWindows();
               wtoken.setVisibleBeforeClientHidden();
           } else if (visible) {
               if (!mAppTransition.isTransitionSet() && mAppTransition.isReady()) {
                   // Add the app mOpeningApps if transition is unset but ready. This means
                   // we're doing a screen freeze, and the unfreeze will wait for all opening
                   // apps to be ready.
                   mOpeningApps.add(wtoken);
               }
               wtoken.startingMoved = false;
               // If the token is currently hidden (should be the common case), or has been
               // stopped, then we need to set up to wait for its windows to be ready.
               if (wtoken.hidden || wtoken.mAppStopped) {
                   wtoken.clearAllDrawn();

                   // If the app was already visible, don't reset the waitingToShow state.
                  //如果hidden的值等于false,說明Activity組件當前是不可見的。又由于上面visible為true,表示Activity將要被設置成可見的,
                  //因此,這時候就需要將AppWindowToken對象wtoken的成員變量waitingToShow的值設置為true。
                   if (wtoken.hidden) {
                       wtoken.waitingToShow = true;
                   }

                   if (wtoken.clientHidden) {
                       // In the case where we are making an app visible
                       // but holding off for a transition, we still need
                       // to tell the client to make its windows visible so
                       // they get drawn.  Otherwise, we will wait on
                       // performing the transition until all windows have
                       // been drawn, they never will be, and we are sad.
                       wtoken.clientHidden = false;
                       //通知應用程序進程將參數token所描述的Activity組件設置為true
                       wtoken.sendAppVisibilityToClients();
                   }
               }
               wtoken.requestUpdateWallpaperIfNeeded();

               if (DEBUG_ADD_REMOVE) Slog.v(
                       TAG_WM, "No longer Stopped: " + wtoken);
               wtoken.mAppStopped = false;
           }
          //這個if分之在動畫設置完成并且屏幕不凍屏,亮屏、Display OK的情況下才會走
           if (okToDisplay() && mAppTransition.isTransitionSet()) {
               if (wtoken.mAppAnimator.usingTransferredAnimation
                       && wtoken.mAppAnimator.animation == null) {
                   Slog.wtf(TAG_WM, "Will NOT set dummy animation on: " + wtoken
                           + ", using null transfered animation!");
               }
               if (!wtoken.mAppAnimator.usingTransferredAnimation &&
                       (!wtoken.startingDisplayed || mSkipAppTransitionAnimation)) {
                   if (DEBUG_APP_TRANSITIONS) Slog.v(
                           TAG_WM, "Setting dummy animation on: " + wtoken);
                  //設置啞動畫,可以理解是一個站位的作用,后面會對它設置真正的動畫
                   wtoken.mAppAnimator.setDummyAnimation();
               }
               wtoken.inPendingTransaction = true;
               if (visible) {
               //可見,把wtoken加入到mOpeningApps
                   mOpeningApps.add(wtoken);
                   wtoken.mEnteringAnimation = true;
               } else {
               //不可見,把wtoken加入到mClosingApps
                   mClosingApps.add(wtoken);
                   wtoken.mEnteringAnimation = false;
               }
               if (mAppTransition.getAppTransition() == AppTransition.TRANSIT_TASK_OPEN_BEHIND) {
                   // We're launchingBehind, add the launching activity to mOpeningApps.
                   final WindowState win = findFocusedWindowLocked(getDefaultDisplayContentLocked());
                   if (win != null) {
                       final AppWindowToken focusedToken = win.mAppToken;
                       if (focusedToken != null) {
                           focusedToken.hidden = true;
                           mOpeningApps.add(focusedToken);
                       }
                   }
               }
               return;
           }
           final long origId = Binder.clearCallingIdentity();
           wtoken.inPendingTransaction = false;
             //將參數token所描述的Activity組件的可見性設置為參數visible所描述的值;
           setTokenVisibilityLocked(wtoken, null, visible, AppTransition.TRANSIT_UNSET,true, wtoken.voiceInteraction);
          //向WMS服務上報參數token所描述的Activity組件的可見性
           wtoken.updateReportedVisibilityLocked();
           Binder.restoreCallingIdentity(origId);
       }
   }

這個方法變量比較多,要全部弄明白,還是要花費一些功夫的。setAppVisibility設置好之后,就可以通知客戶端啟動APP進程了,(所以這樣看來,當一個Activity的實例還不存在的時候,它的窗口的token就已經被確定了)接著往下走,completeResumeLocked方法主要是從上下到檢查哪些Activity組件是需要設置為可見的,哪些Activity組件是需要設置為不可見的,找到棧頂部第一個全屏顯示的Activity組件,調用setAppVisibility設置為true,這個全屏顯示Activity組件下面的所有Activity組件的可見性設置為false。
最后通知WindowManagerService服務調用executeAppTransition方法執行前面所準備的切換操作,執行這個切換操作跟Activity窗口動畫(過度動畫)有關系,現在就開始第二節內容。

2、過度動畫設置
過度動畫設置的設置過程

粗略的看一共19個步驟,前面prepareAppTransition設置切換操作和sendAppVisibility方法設置哪個Activity要隱藏,哪個Activity的要顯示,已經解釋過了,現在從sendAppVisibilityToClient開始。sendAppVisibilityToClient/dispatchAppVibility 這兩個函數就是通知上層應用窗口可見性發生變化。如果下一個Activity是冷啟動,那么這個函數并不能通知下一個Activity的窗口變為可見,因為此時該函數調用時,下一個Activity的窗口還沒加到到WMS中來,Activity的窗口添加是Activity 的onResume方法中添加的。然后到第四步finishDrawingWindow下一個被Resume起來后,添加窗口、measure、layout、draw等一系列操作完成后便會調用WMS.finishDrawingWindow()來通知WMS,該窗口已經繪制好了,可以開始做動畫了。WMS.finishDrawingWindow()會調用WindowStateAnimator.finishDrawingLocked()更新窗口狀態mDrawState為COMMIT_DRAW_PENDING。其次WindowSurfacePlacer的requestTraversal方法,WindowSurfacePlacer的requestTraversal方法只是向WMS的主線程發送了一個DO_TRAVERSAL消息,WMS收到這個消息后,performSurfacePlacement方法就會執行。

frameworks/base/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
final void performSurfacePlacement() {
       if (mDeferDepth > 0) {
           return;
       }
       int loopCount = 6;
       do {
           mTraversalScheduled = false;
           performSurfacePlacementLoop();
           mService.mH.removeMessages(DO_TRAVERSAL);
           loopCount--;
       } while (mTraversalScheduled && loopCount > 0);
       mWallpaperActionPending = false;
 }

序列圖中performSurfacePlacement、performSurfacePlacementLoop、performSurfacePlacementInner三個方法都是跟渲染相關的。performSurfacePlacement中調用了performSurfacePlacementLoop,performSurfacePlacementLoop中調用了performSurfacePlacementInner。(todolist:梳理performSurfacePlacement方法)

第十步commitFinishDrawingLocked是applySurfaceChangesTransaction方法調用進來的,該函數將窗口狀態為COMMIT_DRAW_PENDING或READY_TO_SHOW的窗口,全部更新到READY_TO_SHOW狀態

第十一步updateAllDrawnLocked函數更新AppWindowToken.allDrawn值。只有屬于該AppWindowToken的所有窗口都是繪制完成狀態(一般情況下只有一個窗口,有時候會有父窗口、子窗口,這時屬于該AppWindowToken的窗口數量就不止一個了),AppWindowToken.allDrawn才會置為true。AppWindowToken.allDrawn為true才會使得第十步中的WMS.handleAppTransitionReadyLocked()完整的執行。
第十二步handleAppTransitionReadyLocked主要做了以下幾件事情。

private int handleAppTransitionReadyLocked(WindowList windows) {
       //獲取系統中所有打開的activity數量
       int appsCount = mService.mOpeningApps.size();
       //transitionGoodToGo會判斷多種case情況下,不用執行動畫的情況,
        //比如正在做轉屏動畫,mOpeningApps中任何一個allDrawn不等于true等
       if (!transitionGoodToGo(appsCount)) {
           return 0;
       }
       Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");

       if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "**** GOOD TO GO");
           //獲取前面設置好的切換操作
       int transit = mService.mAppTransition.getAppTransition();
        //如果因為動畫沒有成功設置好,或者因為凍屏等原因,導致的WMS中mSkipAppTransitionAnimation為true的話,切換操作類型設置為TRANSIT_UNSET
       if (mService.mSkipAppTransitionAnimation) {
           transit = AppTransition.TRANSIT_UNSET;
       }
       mService.mSkipAppTransitionAnimation = false;
       mService.mNoAnimationNotifyOnTransitionFinished.clear();
      //這個時候可以移除prepareAppTransition中設置的超時消息
       mService.mH.removeMessages(H.APP_TRANSITION_TIMEOUT);
      //重新進行窗口的排序,防止亂序
       mService.rebuildAppWindowListLocked();

       mWallpaperMayChange = false;

       // The top-most window will supply the layout params,
       // and we will determine it below.
       //用來保存窗口參數
       LayoutParams animLp = null;
       int bestAnimLayer = -1;
       boolean fullscreenAnim = false;
      //是否有語音交互
       boolean voiceInteraction = false;

       int i;
       for (i = 0; i < appsCount; i++) {
           final AppWindowToken wtoken = mService.mOpeningApps.valueAt(i);
           // Clearing the mAnimatingExit flag before entering animation. It's set to
           // true if app window is removed, or window relayout to invisible.
           // This also affects window visibility. We need to clear it *before*
           // maybeUpdateTransitToWallpaper() as the transition selection depends on
           // wallpaper target visibility.
           wtoken.clearAnimatingFlags();

       }
       // Adjust wallpaper before we pull the lower/upper target, since pending changes
       // (like the clearAnimatingFlags() above) might affect wallpaper target result.
       final DisplayContent displayContent = mService.getDefaultDisplayContentLocked();
       if ((displayContent.pendingLayoutChanges & FINISH_LAYOUT_REDO_WALLPAPER) != 0 &&
               mWallpaperControllerLocked.adjustWallpaperWindows()) {
            //上面執行了clearAnimatingFlags,會影響Z-order.這里重新調整一下
           mService.mLayersController.assignLayersLocked(windows);
           displayContent.layoutNeeded = true;
       }

        //在調整壁紙窗口在窗口堆棧的位置的時候,如果碰到系統在執行兩個Activity組件的切換操作,
       //并且這兩個Activity組件都需要顯示壁紙,
       //那么Z軸位置較低的窗口就會lowerWallpaperTarget中,
       //而Z軸位置較高的窗口就會保存在upperWallpaperTarget中。

       final WindowState lowerWallpaperTarget =
               mWallpaperControllerLocked.getLowerWallpaperTarget();
       final WindowState upperWallpaperTarget =
               mWallpaperControllerLocked.getUpperWallpaperTarget();

       boolean openingAppHasWallpaper = false;
       boolean closingAppHasWallpaper = false;
       final AppWindowToken lowerWallpaperAppToken;
       final AppWindowToken upperWallpaperAppToken;
       if (lowerWallpaperTarget == null) {
           lowerWallpaperAppToken = upperWallpaperAppToken = null;
       } else {
           lowerWallpaperAppToken = lowerWallpaperTarget.mAppToken;
           upperWallpaperAppToken = upperWallpaperTarget.mAppToken;
       }

       // Do a first pass through the tokens for two
       // things:
       // (1) Determine if both the closing and opening
       // app token sets are wallpaper targets, in which
       // case special animations are needed
       // (since the wallpaper needs to stay static
       // behind them).
       // (2) Find the layout params of the top-most
       // application window in the tokens, which is
       // what will control the animation theme.
          //獲取關閉的activiy數量
       final int closingAppsCount = mService.mClosingApps.size();
          //獲取打開的activiy數量
       appsCount = closingAppsCount + mService.mOpeningApps.size();
         //這段代碼的for循環就是要從參與切換操作的Activity組件的窗口的WindowManager.LayoutParams對象中挑選出一個來創建切換動畫
        //要求是主窗口,它是所有候選窗口中Z軸位置最高的
       for (i = 0; i < appsCount; i++) {
           final AppWindowToken wtoken;
           if (i < closingAppsCount) {
               wtoken = mService.mClosingApps.valueAt(i);
               if (wtoken == lowerWallpaperAppToken || wtoken == upperWallpaperAppToken) {
           //Activity關閉的時候,要顯示墻紙窗口
                   closingAppHasWallpaper = true;
               }
           } else {
               wtoken = mService.mOpeningApps.valueAt(i - closingAppsCount);
               if (wtoken == lowerWallpaperAppToken || wtoken == upperWallpaperAppToken) {
                //Activity打開的時候,要顯示墻紙窗口
                   openingAppHasWallpaper = true;
               }
           }

           voiceInteraction |= wtoken.voiceInteraction;
             //是否是全屏
           if (wtoken.appFullscreen) {
            //找到主窗口,類型為TYPE_BASE_APPLICATION或者TYPE_APPLICATION_STARTING類型的
               WindowState ws = wtoken.findMainWindow();
               if (ws != null) {
                   animLp = ws.mAttrs;
                   bestAnimLayer = ws.mLayer;
                   fullscreenAnim = true;
               }
           } else if (!fullscreenAnim) {
               WindowState ws = wtoken.findMainWindow();
               if (ws != null) {
                   if (ws.mLayer > bestAnimLayer) {
                       animLp = ws.mAttrs;
                       bestAnimLayer = ws.mLayer;
                   }
               }
           }
       }

        //判斷切換操作跟墻紙類型是否相關,調整窗口的類型(mayUpdateTransitionToWallpaper)
       transit = maybeUpdateTransitToWallpaper(transit, openingAppHasWallpaper,
               closingAppHasWallpaper, lowerWallpaperTarget, upperWallpaperTarget);

       // If all closing windows are obscured, then there is
       // no need to do an animation.  This is the case, for
       // example, when this transition is being done behind
       // the lock screen.
       if (!mService.mPolicy.allowAppAnimationsLw()) {
           if (DEBUG_APP_TRANSITIONS) Slog.v(TAG,
                   "Animations disallowed by keyguard or dream.");
           animLp = null;
       }

       processApplicationsAnimatingInPlace(transit);

       mTmpLayerAndToken.token = null;
       // MIUI ADD:
       mService.mAppTransition.updateAllowCustomAnimationIfNeeded(mService.mClosingApps);
      //處理關閉的Activity
       handleClosingApps(transit, animLp, voiceInteraction, mTmpLayerAndToken);
       final AppWindowToken topClosingApp = mTmpLayerAndToken.token;
       final int topClosingLayer = mTmpLayerAndToken.layer;
        //處理打開的Activity,下面會說
       final AppWindowToken topOpeningApp = handleOpeningApps(transit,
               animLp, voiceInteraction, topClosingLayer);

       mService.mAppTransition.setLastAppTransition(transit, topOpeningApp, topClosingApp);

       final AppWindowAnimator openingAppAnimator = (topOpeningApp == null) ?  null :
               topOpeningApp.mAppAnimator;
       final AppWindowAnimator closingAppAnimator = (topClosingApp == null) ? null :
               topClosingApp.mAppAnimator;

       mService.mAppTransition.goodToGo(openingAppAnimator, closingAppAnimator,
               mService.mOpeningApps, mService.mClosingApps);
       mService.mAppTransition.postAnimationCallback();
       mService.mAppTransition.clear();

       mService.mOpeningApps.clear();
       mService.mClosingApps.clear();

       .....

       return FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_CONFIG;
   }

總體來說大致有八個步驟

  • 1、調用條件:首先判斷是否超時,超時了不執行,判斷mOpeningApps中每一個AppWindowToken的allDrawn值是否為true
  • 2、判斷墻紙是否需要可見,如果需要,先繪制墻紙,在走切換動畫邏輯
  • 3、取出mAppTransition的切換操作,移除超時消息
  • 4、窗口堆棧順序重排,rebuildAppWindowListLocked
  • 5、取得頂層全屏窗口的mAttr值(LayoutParams),記錄在animLp
  • 6、判斷切換操作跟墻紙類型是否相關,調整類型(mayUpdateTransitionToWallpaper)
  • 7、分別處理handleClosingApps/handleOpeningApps
  • 8、清理工作

第十三步handleOpeningApps這個函數用來設置APPWindowToken.hidden的可見性、設置Activity切換動畫(如果參數transit==AppTransition.TRANSIT_UNSET,那就是會設置窗口動畫,否則就會設置Activity切換動畫),如果存在Activity切換動畫或屬于該Activity的窗口正在做窗口動畫,那么返回值為true,handleOpeningApps中調用了setTokenVisibilityLocked方法。

/frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
boolean setTokenVisibilityLocked(AppWindowToken wtoken, WindowManager.LayoutParams lp,
           boolean visible, int transit, boolean performLayout, boolean isVoiceInteraction) {
       boolean delayed = false;

       if (wtoken.clientHidden == visible) {
           wtoken.clientHidden = !visible;
            //再次通知應用程序端設置窗口可見性
           wtoken.sendAppVisibilityToClients();
       }
       boolean visibilityChanged = false;
       if (wtoken.hidden == visible || (wtoken.hidden && wtoken.mIsExiting) ||
               (visible && wtoken.waitingForReplacement())) {
           boolean changed = false;
           boolean runningAppAnimation = false;
           if (transit != AppTransition.TRANSIT_UNSET) {
               if (wtoken.mAppAnimator.animation == AppWindowAnimator.sDummyAnimation) {
                   //把前面的啞動畫清空
                   wtoken.mAppAnimator.setNullAnimation();
               }
               //創建動畫
               if (applyAnimationLocked(wtoken, lp, transit, visible, isVoiceInteraction)) {
                   delayed = runningAppAnimation = true;
               }
               WindowState window = wtoken.findMainWindow();
               if (window != null && mAccessibilityController != null
                       && window.getDisplayId() == Display.DEFAULT_DISPLAY) {
                   mAccessibilityController.onAppWindowTransitionLocked(window, transit);
               }
               changed = true;
           }
        .......
     }
     .......
       return delayed;
   }

創建動畫是applyAnimationLocked方法干的事情。

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
private boolean applyAnimationLocked(AppWindowToken atoken, WindowManager.LayoutParams lp,
           int transit, boolean enter, boolean isVoiceInteraction) {
       // Only apply an animation if the display isn't frozen.  If it is
       // frozen, there is no reason to animate and it can cause strange
       // artifacts when we unfreeze the display if some different animation
       // is running.
       Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "WM#applyAnimationLocked");
       if (okToDisplay()) {
           ....
           //傳入各種參數,用AppTransition的loadAnimation創建一個動畫
           Animation a = mAppTransition.loadAnimation(lp, transit, enter, mCurConfiguration.uiMode,
                   mCurConfiguration.orientation, frame, displayFrame, insets, surfaceInsets,
                   isVoiceInteraction, freeform, atoken.mTask.mTaskId, mIsInMultiWindowMode);
          ....
           if (a != null) {
                  ....
               atoken.mAppAnimator.setAnimation(a, containingWidth, containingHeight,
                       mAppTransition.canSkipFirstFrame(), mAppTransition.getAppStackClipMode());
               ....
           }
       } else {
           atoken.mAppAnimator.clearAnimation();
       }
       return atoken.mAppAnimator.animation != null;
   }

applyAnimationLocked內部實質上還是調用loadAnimation。

frameworks/base/services/core/java/com/android/server/wm/AppTransition.java
Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, int uiMode,
           int orientation, Rect frame, Rect displayFrame, Rect insets,
           @Nullable Rect surfaceInsets, boolean isVoiceInteraction, boolean freeform,
           int taskId, boolean isInMultiWindowMode) {

       if (transit == TRANSIT_WALLPAPER_OPEN && mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM
               && !mAllowCustomAnimation) {
           mNextAppTransitionType = AppTransitionInjector.NEXT_TRANSIT_TYPE_BACK_HOME;
       }
       Animation a;
       if (isVoiceInteraction && (transit == TRANSIT_ACTIVITY_OPEN
               || transit == TRANSIT_TASK_OPEN
               || transit == TRANSIT_TASK_TO_FRONT)) {
           a = loadAnimationRes(lp, enter
                   ? com.android.internal.R.anim.voice_activity_open_enter
                   : com.android.internal.R.anim.voice_activity_open_exit);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation voice:"
                   + " anim=" + a + " transit=" + appTransitionToString(transit)
                   + " isEntrance=" + enter + " Callers=" + Debug.getCallers(3));
       } else if (isVoiceInteraction && (transit == TRANSIT_ACTIVITY_CLOSE
               || transit == TRANSIT_TASK_CLOSE
               || transit == TRANSIT_TASK_TO_BACK)) {
           a = loadAnimationRes(lp, enter
                   ? com.android.internal.R.anim.voice_activity_close_enter
                   : com.android.internal.R.anim.voice_activity_close_exit);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation voice:"
                   + " anim=" + a + " transit=" + appTransitionToString(transit)
                   + " isEntrance=" + enter + " Callers=" + Debug.getCallers(3));
       } else if (transit == TRANSIT_ACTIVITY_RELAUNCH) {
           a = createRelaunchAnimation(frame, insets);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                   + " anim=" + a + " nextAppTransition=" + mNextAppTransition
                   + " transit=" + appTransitionToString(transit)
                   + " Callers=" + Debug.getCallers(3));
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM) {
           a = loadAnimationRes(mNextAppTransitionPackage, enter ?
                   mNextAppTransitionEnter : mNextAppTransitionExit);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                   + " anim=" + a + " nextAppTransition=ANIM_CUSTOM"
                   + " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
                   + " Callers=" + Debug.getCallers(3));
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE) {
           a = loadAnimationRes(mNextAppTransitionPackage, mNextAppTransitionInPlace);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                   + " anim=" + a + " nextAppTransition=ANIM_CUSTOM_IN_PLACE"
                   + " transit=" + appTransitionToString(transit)
                   + " Callers=" + Debug.getCallers(3));
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CLIP_REVEAL) {
           a = createClipRevealAnimationLocked(transit, enter, frame, displayFrame);
           mLauncherAnimationRect.setEmpty();
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                           + " anim=" + a + " nextAppTransition=ANIM_CLIP_REVEAL"
                           + " transit=" + appTransitionToString(transit)
                           + " Callers=" + Debug.getCallers(3));
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_SCALE_UP) {
           a = createScaleUpAnimationLocked(transit, enter, frame);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                   + " anim=" + a + " nextAppTransition=ANIM_SCALE_UP"
                   + " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
                   + " Callers=" + Debug.getCallers(3));
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP ||
               mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN) {
           mNextAppTransitionScaleUp =
                   (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP);
           a = createThumbnailEnterExitAnimationLocked(getThumbnailTransitionState(enter),
                   frame, transit, taskId);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
               String animName = mNextAppTransitionScaleUp ?
                       "ANIM_THUMBNAIL_SCALE_UP" : "ANIM_THUMBNAIL_SCALE_DOWN";
               Slog.v(TAG, "applyAnimation:"
                       + " anim=" + a + " nextAppTransition=" + animName
                       + " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
                       + " Callers=" + Debug.getCallers(3));
           }
       } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP ||
               mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN) {
           mNextAppTransitionScaleUp =
                   (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP);
           a = createAspectScaledThumbnailEnterExitAnimationLocked(
                   getThumbnailTransitionState(enter), uiMode, orientation, transit, frame,
                   insets, surfaceInsets, freeform, taskId);
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
               String animName = mNextAppTransitionScaleUp ?
                       "ANIM_THUMBNAIL_ASPECT_SCALE_UP" : "ANIM_THUMBNAIL_ASPECT_SCALE_DOWN";
               Slog.v(TAG, "applyAnimation:"
                       + " anim=" + a + " nextAppTransition=" + animName
                       + " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
                       + " Callers=" + Debug.getCallers(3));
           }
       } else {
           int animAttr = 0;
           switch (transit) {
               case TRANSIT_ACTIVITY_OPEN:
                   animAttr = enter
                           ? WindowAnimation_activityOpenEnterAnimation
                           : WindowAnimation_activityOpenExitAnimation;
                   break;
               case TRANSIT_ACTIVITY_CLOSE:
                   animAttr = enter
                           ? WindowAnimation_activityCloseEnterAnimation
                           : WindowAnimation_activityCloseExitAnimation;
                   break;
               case TRANSIT_DOCK_TASK_FROM_RECENTS:
               case TRANSIT_TASK_OPEN:
                   animAttr = enter
                           ? WindowAnimation_taskOpenEnterAnimation
                           : WindowAnimation_taskOpenExitAnimation;
                   break;
               case TRANSIT_TASK_CLOSE:
                   animAttr = enter
                           ? WindowAnimation_taskCloseEnterAnimation
                           : WindowAnimation_taskCloseExitAnimation;
                   break;
               case TRANSIT_TASK_TO_FRONT:
                   animAttr = enter
                           ? WindowAnimation_taskToFrontEnterAnimation
                           : WindowAnimation_taskToFrontExitAnimation;
                   break;
               case TRANSIT_TASK_TO_BACK:
                   animAttr = enter
                           ? WindowAnimation_taskToBackEnterAnimation
                           : WindowAnimation_taskToBackExitAnimation;
                   break;
               case TRANSIT_WALLPAPER_OPEN:
                   animAttr = enter
                           ? WindowAnimation_wallpaperOpenEnterAnimation
                           : WindowAnimation_wallpaperOpenExitAnimation;
                   break;
               case TRANSIT_WALLPAPER_CLOSE:
                   animAttr = enter
                           ? WindowAnimation_wallpaperCloseEnterAnimation
                           : WindowAnimation_wallpaperCloseExitAnimation;
                   break;
               case TRANSIT_WALLPAPER_INTRA_OPEN:
                   animAttr = enter
                           ? WindowAnimation_wallpaperIntraOpenEnterAnimation
                           : WindowAnimation_wallpaperIntraOpenExitAnimation;
                   break;
               case TRANSIT_WALLPAPER_INTRA_CLOSE:
                   animAttr = enter
                           ? WindowAnimation_wallpaperIntraCloseEnterAnimation
                           : WindowAnimation_wallpaperIntraCloseExitAnimation;
                   break;
               case TRANSIT_TASK_OPEN_BEHIND:
                   animAttr = enter
                           ? WindowAnimation_launchTaskBehindSourceAnimation
                           : WindowAnimation_launchTaskBehindTargetAnimation;
           }
           a = animAttr != 0 ? loadAnimationAttr(lp, animAttr) : null;
           if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation:"
                   + " anim=" + a
                   + " animAttr=0x" + Integer.toHexString(animAttr)
                   + " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
                   + " Callers=" + Debug.getCallers(3));
       }
       return a;
   }

關于loadAnimation這個方法代碼也是很多,但是邏輯非常簡單了,就是根據設置的操作類型, 根據參數,使用loadAnimationRes()或loadAnimationAttr()或其他創建Animation接口來加載一個Animation出來。加載動畫的時候需要注意一個優先級的問題。

  • 如果開啟語音交互,則根據mNextAppTransition類型返回對應動畫,由AMS設置的動畫類型(由Activity發生的行為決定)如TRANSIT_ACTIVITY_OPEN表示當前發生了Activity打開的行為
  • 如果設置了某種mNextAppTransitionType類型,則根據此類型返回對應動畫。:由客戶端進程設置的動畫類型(由客戶端決定) 如Activity#overridePendingTransition,在決定要為當前窗口設置何種動畫時,此類型的優先級高于第一種。
  • 根據mNextAppTransition類型返回對應動畫。
  • 沒有任何條件滿足,返回空

比如Activity關閉的時候,加載的動畫資源是下面這樣

<set xmlns:android="http://schemas.android.com/apk/res/android" android:zAdjustment="normal">
   <alpha android:fromAlpha="0.7" android:toAlpha="1.0"
           android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true"
           android:interpolator="@interpolator/linear_out_slow_in"
           android:duration="250"/>
</set>

動畫設置好了之后,就會通過setAnimation方法將動畫anim保存在AppWindowAnimator的成員變量animation中,動畫的執行時候,就會來取這個animation。

frameworks/base/services/core/java/com/android/server/wm/AppWindowAnimator.java
public void setAnimation(Animation anim, int width, int height, boolean skipFirstFrame,
           int stackClip) {
       animation = anim;
       animating = false;
       if (!anim.isInitialized()) {
           anim.initialize(width, height, width, height);
       }
     ....
   }

剛剛在說動畫的優先級的時候,說過如果設置了某種mNextAppTransitionType類型,就跟以這個mNextAppTransitionType類型作為返回,優先級高于第一種。這個過程的時序圖如下。


過度動畫
frameworks/base/services/core/java/com/android/server/wm/AppTransition.java

 void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim,
           IRemoteCallback startedCallback) {
       if (isTransitionSet()) {
           clear();
           mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM;
           mNextAppTransitionPackage = packageName;
           mNextAppTransitionEnter = enterAnim;
           mNextAppTransitionExit = exitAnim;
           postAnimationCallback();
           mNextAppTransitionCallback = startedCallback;
       } else {
           postAnimationCallback();
       }
   }

mNextAppTransitionType被覆蓋了之后,創建動畫的時候就會優先返回設置了這種類型的動畫。

3、窗口動畫設置
image.png

相對與過度動畫,窗口動畫的設置過程會簡單一些,從commitFinishDrawingLocked方法說起,commitFinishDrawingLocked是也是從performSurfacePlacementInner里面調用而來的。

   boolean commitFinishDrawingLocked() {
        .....
       mDrawState = READY_TO_SHOW;
       boolean result = false;
       final AppWindowToken atoken = mWin.mAppToken;
       //去取出atoken,如果atoken等于null,那么說明不是Activity窗口,就可以調用performShowLocked,進行窗口動畫的設置
       if (atoken == null || atoken.allDrawn || mWin.mAttrs.type == TYPE_APPLICATION_STARTING) {
           result = performShowLocked();
       }
       return result;
   }

performShowLocked中主要是調用了applyEnterAnimationLocked方法進行創建動畫。

   void applyEnterAnimationLocked() {
       // If we are the new part of a window replacement transition and we have requested
       // not to animate, we instead want to make it seamless, so we don't want to apply
       // an enter transition.
       if (mWin.mSkipEnterAnimationForSeamlessReplacement) {
           return;
       }
       final int transit;
       if (mEnterAnimationPending) {
           mEnterAnimationPending = false;
           transit = WindowManagerPolicy.TRANSIT_ENTER;
       } else {
           transit = WindowManagerPolicy.TRANSIT_SHOW;
       }
       applyAnimationLocked(transit, true);
     .....
   }

mEnterAnimationPending的值等于true,說明當前所描述的窗口正在等待顯示,也就是正處于不可見到可見狀態的過程中,那WindowManagerService類的成員函數applyEnterAnimationLocked就會對該窗口設置一個類型為WindowManagerPolicy.TRANSIT_ENTER的動畫,否則的話,就會對該窗口設置一個類型為WindowManagerPolicy.TRANSIT_SHOW的動畫。后面會根據這個類型,確定styleable, 將參數transit的值轉化為一個對應的動畫樣式名稱。

frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java
boolean applyAnimationLocked(int transit, boolean isEntrance) {
       ......
       if (mService.okToDisplay()) {
           int anim = mPolicy.selectAnimationLw(mWin, transit);
           int attr = -1;
           Animation a = null;
           if (anim != 0) {
               a = anim != -1 ? AnimationUtils.loadAnimation(mContext, anim) : null;
           } else {
               switch (transit) {
                   case WindowManagerPolicy.TRANSIT_ENTER:
                       attr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
                       break;
                   case WindowManagerPolicy.TRANSIT_EXIT:
                       attr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
                       break;
                   case WindowManagerPolicy.TRANSIT_SHOW:
                       attr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation;
                       break;
                   case WindowManagerPolicy.TRANSIT_HIDE:
                       attr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation;
                       break;
               }
               if (attr >= 0) {
                   a = mService.mAppTransition.loadAnimationAttr(mWin.mAttrs, attr);
               }
           }
           if (DEBUG_ANIM) Slog.v(TAG,
                   "applyAnimation: win=" + this
                   + " anim=" + anim + " attr=0x" + Integer.toHexString(attr)
                   + " a=" + a
                   + " transit=" + transit
                   + " isEntrance=" + isEntrance + " Callers " + Debug.getCallers(3));
           if (a != null) {
               if (DEBUG_ANIM) logWithStack(TAG, "Loaded animation " + a + " for " + this);
               setAnimation(a);
               mAnimationIsEntrance = isEntrance;
           }
       } else {
           clearAnimation();
       }
       Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);

       if (mWin.mAttrs.type == TYPE_INPUT_METHOD) {
           mService.adjustForImeIfNeeded(mWin.mDisplayContent);
           if (isEntrance) {
               mWin.setDisplayLayoutNeeded();
               mService.mWindowPlacerLocked.requestTraversal();
           }
       }
       return mAnimation != null;
   }

首先會調用PhoneWindowManager的selectAnimationLw方法去查找特殊窗口的動畫類型,這里特殊窗口主要是StatusBar、NavigationBar或者窗口的類型是TYPE_DOCK_DIVIDER(分屏)等,如果是這些窗口的話,就會直接返回一個動畫類型(transit)保存在anim中,接下來會判斷anim是否為-1,因為selectAnimationLw在窗口是Keyguard或者DREAM類型的時候會返回-1,如果不是-1,說明查找到了,返回到WindowStateAnimator中使用AnimationUtils的loadAnimation方法去查找出一個動畫a保存在Animation所描述的變量a中。

如果上面都不是,那么就根據transit類型,確定attr,調用AppTransition的loadAnimationAttr方法加載一個動畫

frameworks/base/services/core/java/com/android/server/wm/AppTransition.java
   Animation loadAnimationAttr(WindowManager.LayoutParams lp, int animAttr) {
       int anim = 0;
       Context context = mContext;
       if (animAttr >= 0) {
           AttributeCache.Entry ent = getCachedAnimations(lp);
           if (ent != null) {
               context = ent.context;
               anim = ent.array.getResourceId(animAttr, 0);
           }
       }
       if (anim != 0) {
           return AnimationUtils.loadAnimation(context, anim);
       }
       return null;
   }
frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java
 public void setAnimation(Animation anim, long startTime, int stackClip) {
       if (localLOGV) Slog.v(TAG, "Setting animation in " + this + ": " + anim);
       mAnimating = false;
       mLocalAnimating = false;
       mAnimation = anim;
       ...
   }

最后動畫被保存在WindowStateAnimator的成員變量mAnimation中。對比前面的過度動畫,最后是
通過setAnimation方法將動畫anim保存在AppWindowAnimator的成員變量animation中。當動畫的執行時候,就會來取這個animation,動畫的執行,放在接下來一節中更新。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容