Android中的基礎(chǔ)動畫 屬性動畫(Property Animation)

導(dǎo)讀

Android中的基礎(chǔ)動畫 屬性動畫(Property Animation)

通過前面的文章Android中的視圖動畫(View Animation)(View動畫贷笛、補(bǔ)間動畫)我們知道視圖動畫只是改變了View的視覺效果靶庙,而實(shí)際并未變更,而屬性動畫可謂是視圖動畫的加強(qiáng)版艾栋,并且具有更好的特性爆存,因為屬性動畫不僅改變了視覺效果,而且實(shí)際也跟隨變動了蝗砾,并保留了視圖動畫如監(jiān)聽等功能先较。

舉個不恰當(dāng)?shù)睦樱?br> 彭空空做夢賺了一卡車的人民幣,實(shí)際收入呢遥诉,0元拇泣;
馬云大佬做夢賺了一卡車的人民幣,實(shí)際收入一卡車的人民幣。

這里我寫了兩段簡單的代碼矮锈,一個是屬性動畫霉翔,一個是補(bǔ)間動畫,效果均是讓Button平移的效果苞笨,并對兩個Button設(shè)置了點(diǎn)擊事件债朵,以下是關(guān)鍵代碼:

private void showTweenAnim() {
        Animation translateAnimation = new TranslateAnimation(0, 800, 0, 0);
        translateAnimation.setDuration(3000);
        translateAnimation.setRepeatCount(-1);
        button2.startAnimation(translateAnimation);
    }
private void showObjectAnimatorOfFloat() {
        ObjectAnimator animator = ObjectAnimator.ofFloat(button1, "translationX", 0, 500);
        animator.setDuration(3000);
        animator.setRepeatCount(-1);
        animator.start();
    }
屬性動畫和補(bǔ)間動畫

這是以上代碼的動畫效果和點(diǎn)擊事件的響應(yīng),可以看到Button從左邊移動到右邊瀑凝,屬性動畫一直可以響應(yīng)點(diǎn)擊事件序芦,而補(bǔ)間動畫只有在原來的位置才響應(yīng)事件。

再看兩個動畫的代碼粤咪,對比發(fā)現(xiàn)谚中,除了構(gòu)造(靜態(tài)方法)基本上一致,而我們知道補(bǔ)間動畫要實(shí)現(xiàn)平移、選擇宪塔、縮放磁奖、透明度的動畫,分別需要TranslateAnimation(平移動畫)某筐、RotateAnimation(旋轉(zhuǎn)動畫)比搭、ScaleAnimation(縮放動畫)、AlphaAnimation(透明度動畫)這些類南誊,而上面的代碼中身诺,屬性動畫使用了ObjectAnimator的ofFloat()方法,后面?zhèn)魅腙P(guān)鍵參數(shù)“translationX”抄囚,就實(shí)現(xiàn)了平移動畫霉赡,看來屬性動畫內(nèi)部進(jìn)行了擴(kuò)展性的封裝,這里就不去具體研究是如何封裝的了怠苔。

下面具體來看看ObjectAnimator.ofFloat()方法:

 public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setIntValues(values);
        return anim;
    }
  • new ObjectAnimator(target, propertyName)
    ofInt(Object target, String propertyName, int... values)方法接收三個參數(shù)同廉。先把前兩個參數(shù)傳遞給了ObjectAnimator構(gòu)造方法:
    private ObjectAnimator(Object target, String propertyName) {
            setTarget(target);
            setPropertyName(propertyName);
        }
    
    構(gòu)造方法里先是執(zhí)行了setTarget(target)方法,根據(jù)命名可以看出是進(jìn)行綁定的操作的柑司,內(nèi)部進(jìn)行了一個oldTarget的比對,并且內(nèi)部使用到了弱引用锅劝,這里貼出代碼來:
      @Override
        public void setTarget(@Nullable Object target) {
            final Object oldTarget = getTarget();
            if (oldTarget != target) {
                if (isStarted()) {
                    cancel();
                }
                mTarget = target == null ? null : new WeakReference<Object>(target);
                // New target should cause re-initialization prior to starting
                mInitialized = false;
            }
        }
    

    引用類型是java中比較重要的概念攒驰,可查看Java引用類型

  • setPropertyName(propertyName)方法:
      public void setPropertyName(@NonNull String propertyName) {
          // mValues could be null if this is being constructed piecemeal. Just record the
          // propertyName to be used later when setValues() is called if so.
          if (mValues != null) {
              PropertyValuesHolder valuesHolder = mValues[0];
              String oldName = valuesHolder.getPropertyName();
              valuesHolder.setPropertyName(propertyName);
              mValuesMap.remove(oldName);
              mValuesMap.put(propertyName, valuesHolder);
          }
          mPropertyName = propertyName;
          // New property/values/target should cause re-initialization prior to starting
          mInitialized = false;
      }
    
    這里這里先對mValues進(jìn)行了非空判斷,如果不為空故爵,就會把mValues的第一個參數(shù)取出來作為PropertyValuesHolder玻粪,并綁定propertyName,再存儲到mValuesMap诬垂,進(jìn)行了存儲劲室,那么PropertyValuesHolder是什么呢:
   /**
    * This class holds information about a property and the values that that property
    * should take on during an animation. PropertyValuesHolder objects can be used to create
    * animations with ValueAnimator or ObjectAnimator that operate on several different properties
    * in parallel.
    */
   public class PropertyValuesHolder implements Cloneable {...}

通過注釋,我們得知PropertyValuesHolder是用來封裝屬性相關(guān)的變量:


PropertyValuesHolder的相關(guān)變量
  • setFloatValues(float... values)方法:
     @Override
      public void setFloatValues(float... values) {
          if (mValues == null || mValues.length == 0) {
              // No values yet - this animator is being constructed piecemeal. Init the values with
              // whatever the current propertyName is
              if (mProperty != null) {
                  setValues(PropertyValuesHolder.ofFloat(mProperty, values));
              } else {
                  setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
              }
          } else {
              super.setFloatValues(values);
          }
      }
    
    以及父類ValueAnimator的setFloatValues(float... values)方法:
      public void setFloatValues(float... values) {
          if (values == null || values.length == 0) {
              return;
          }
          if (mValues == null || mValues.length == 0) {
              setValues(PropertyValuesHolder.ofFloat("", values));
          } else {
              PropertyValuesHolder valuesHolder = mValues[0];
              valuesHolder.setFloatValues(values);
          }
          // New property/values/target should cause re-initialization prior to starting
          mInitialized = false;
      }
    
    關(guān)注兩個方法:
    • PropertyValuesHolder.ofFloat("", values)
      可以看到结窘,子類和父類的setFloatValues(float... values)方法很洋,最終都會執(zhí)行setValues方法,這里先追蹤PropertyValuesHolder.ofFloat()隧枫,由于跳轉(zhuǎn)較多喉磁,這里貼出最終的核心代碼:(Keyframes接口的實(shí)現(xiàn)類KeyframeSet中的一段核心代碼)
         public static KeyframeSet ofFloat(float... values) {
                boolean badValue = false;
                int numKeyframes = values.length;
                FloatKeyframe keyframes[] = new               FloatKeyframe[Math.max(numKeyframes,2)];
              if (numKeyframes == 1) {
                    keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
                    keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
                    if (Float.isNaN(values[0])) {
                        badValue = true;
                    }
                } else {
                    keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
                    for (int i = 1; i < numKeyframes; ++i) {
                        keyframes[i] =
                                (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
                    if (Float.isNaN(values[i])) {
                        badValue = true;
                    }
                }
            }
            if (badValue) {
                Log.w("Animator", "Bad value (NaN) in float animator");
            }
            return new FloatKeyframeSet(keyframes);
        }
      
      這段代碼是要把傳遞過來的values進(jìn)行FloatKeyframe轉(zhuǎn)換為2個幀,這就是前面說到的開始幀(開始狀態(tài))官脓、和結(jié)束幀(結(jié)束狀態(tài))协怒。
    • setValues(PropertyValuesHolder... values)
         public void setValues(PropertyValuesHolder... values) {
           int numValues = values.length;
           mValues = values;
           mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
           for (int i = 0; i < numValues; ++i) {
               PropertyValuesHolder valuesHolder = values[i];
               mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
           }
           // New property/values/target should cause re-initialization prior to starting
           mInitialized = false;
       }
      
      最終是把values的所有參數(shù)取出來,作為PropertyValuesHolder存儲到了mValuesMap中卑笨。

到這里孕暇,準(zhǔn)備工作就做完了,即為target準(zhǔn)備propertyName的動畫,把傳遞過來的values轉(zhuǎn)換為系統(tǒng)識別的開始幀妖滔、結(jié)束幀隧哮。


setDuration()、setRepeatCount()铛楣、就不展開了近迁,主要看看start()方法:

    @Override
    public void start() {
        AnimationHandler.getInstance().autoCancelBasedOn(this);
        if (DBG) {...}
        super.start();
    }

新出現(xiàn)一個以Handler命名的類AnimationHandler,由于后面是getInstance()方法簸州,我們大膽猜測這是一個單例鉴竭,單列模式是編程中常見的設(shè)計模式,可查看單例的相關(guān)知識岸浑。跟著后面的autoCancelBasedOn()顧明思意應(yīng)該就是用于動畫取消搏存,保證即將執(zhí)行的動畫的唯一性,這里也不展開了矢洲,先看看AnimationHandler的定義:

  /**
   * This custom, static handler handles the timing pulse that is shared by all active
   * ValueAnimators. This approach ensures that the setting of animation values will happen on the
   * same thread that animations start on, and that all animations will share the same times for
   * calculating their values, which makes synchronizing animations possible.
   *
   * The handler uses the Choreographer by default for doing periodic callbacks. A custom
   * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
   * may be independent of UI frame update. This could be useful in testing.
   *
   * @hide
   */
  public class AnimationHandler {}

大概意思是說AnimationHandler主要是用于處理所有活動的屬性動畫共享的“時間脈沖”璧眠,這個時間脈沖即從開始到結(jié)束每個時間段的“值”,AnimationHandler保證了一個動畫的完整播放都是發(fā)生在同一個線程读虏,該處理程序默認(rèn)情況下使用Choreographer進(jìn)行定期回調(diào)责静。 可以在處理程序上設(shè)置自定義AnimationFrameCallbackProvider,以提供可能獨(dú)立于UI框架更新的定時脈沖盖桥。由于該類非常重要灾螃,所以后面還會涉及到該類下的其他方法。

知道了AnimationHandler具有重要的管理的作用后揩徊,繼續(xù)追蹤父類ValueAnimator的start()方法:

   private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        ...
        addAnimationCallback(0);
        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            startAnimation();
            if (mSeekFraction == -1) {
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }

在start()方法中關(guān)注這些:

  • 對Looper的非空判斷
    Looper是Android中非常重要的類腰鬼,可查看有關(guān)Looper的相關(guān)文章

  • addAnimationCallback(0)

       private void addAnimationCallback(long delay) {
            ...
            getAnimationHandler().addAnimationFrameCallback(this, delay);
        }
    

    這里拿到了具有管理功能的AnimationHandler單例對象塑荒,并且前面說到AnimationHandler是很重要的類熄赡,那么這里深入看看這里添加的回調(diào):

         /**
         *  Register to get a callback on the next frame after the delay.
         */
        public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
            if (mAnimationCallbacks.size() == 0) {
                getProvider().postFrameCallback(mFrameCallback);
            }
            ...
        }
    

    方法注釋說,注冊以獲取延遲后下一幀的回調(diào)齿税,然后方法中執(zhí)行了postFrameCallback(mFrameCallback)方法彼硫,這里重點(diǎn)關(guān)注mFrameCallback:

        private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                doAnimationFrame(getProvider().getFrameTime());
                if (mAnimationCallbacks.size() > 0) {
                    getProvider().postFrameCallback(this);
                }
            }
        };
    

    這里有出現(xiàn)了前面提到的類:Choreographer

      /**
       * Coordinates the timing of animations, input and drawing.
       *...
       */
      public final class Choreographer {
          /**
         * Implement this interface to receive a callback when a new display frame is
         * being rendered.  The callback is invoked on the {@link Looper} thread to
         * which the {@link Choreographer} is attached.
         */
        public interface FrameCallback {
            /**
             * Called when a new display frame is being rendered.
             * ...
             */
            public void doFrame(long frameTimeNanos);
        }
      }
    

    由于注釋都很長,這里貼出關(guān)鍵注釋和其含義:

    • class Choreographer :*協(xié)調(diào)動畫偎窘,輸入和繪圖的時間乌助。
    • interface FrameCallback :實(shí)現(xiàn)此接口以在呈現(xiàn)新的顯示框架時接收回調(diào)。
    • void doFrame :在渲染新的顯示框架時調(diào)用陌知。

    Choreographer翻譯過來是“編舞”的意思他托,舞蹈其實(shí)就是一個動作一個動作的組合,而動畫也是一幀一幀的組合仆葡,而通過上面的注釋來看赏参,即在動畫中每次渲染的時候Choreographer都會執(zhí)行FrameCallback接口的doFrame志笼,似乎確實(shí)有“編舞”之意。既然如此把篓,我們來驗證一下是不是每次渲染時都要調(diào)用該回調(diào):

1纫溃、以Debug模式運(yùn)行
2、在start()方法上打上斷點(diǎn)
3韧掩、在動畫中添加addUpdateListener監(jiān)聽紊浩,并打上斷點(diǎn)
4、在AnimationHandler類下的mFrameCallback中打上斷點(diǎn)
5疗锐、點(diǎn)擊執(zhí)行動畫
6坊谁、通過點(diǎn)擊resume跳到下一個斷點(diǎn),


調(diào)試1.png

調(diào)試2.png

這里需要注意 doAnimationFrame 的斷點(diǎn)滑臊,必須要在后面打上口芍,而不是一開始打上
通過debug我們會發(fā)現(xiàn),doAnimationFrame之后就會調(diào)用addUpdateListener雇卷,然后重復(fù)如此鬓椭,一直到動畫結(jié)束,并且越簡單的動畫关划,重復(fù)次數(shù)越少悄蕾,反之則重復(fù)次數(shù)越多片任,這里可以通過setDuration(30)和setDuration(3000)進(jìn)行對比熟史。
整個流程就是:通過getAnimationHandler().addAnimationFrameCallback(this, delay)進(jìn)行回調(diào)綁定钧萍,這個回調(diào)就是父類ValueAnimator類實(shí)現(xiàn)的AnimationHandler類中的AnimationFrameCallback回調(diào),AnimationFrameCallback中的方法doAnimationFrame()在Choreographer類的FrameCallback回調(diào)中的方法doFrame()中被執(zhí)行脱货。

到這里的結(jié)論就是Choreographer通過調(diào)用doAnimationFrame()來驅(qū)動動畫執(zhí)行每一個關(guān)鍵幀。

  • startAnimation():

      private void startAnimation() {
          if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
              Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                      System.identityHashCode(this));
          }
    
          mAnimationEndRequested = false;
          initAnimation();
          mRunning = true;
          if (mSeekFraction >= 0) {
              mOverallFraction = mSeekFraction;
          } else {
              mOverallFraction = 0f;
          }
          if (mListeners != null) {
              notifyStartListeners();
          }
      }
    
       void initAnimation() {
            if (!mInitialized) {
                int numValues = mValues.length;
                for (int i = 0; i < numValues; ++i) {
                    mValues[i].init();
                }
                mInitialized = true;
            }
        }
    

    在前文中我們知道m(xù)Values其實(shí)就是PropertyValuesHolder律姨,也就是說 initAnimation的目的是初始化PropertyValuesHolder:

        void init() {
            if (mEvaluator == null) {
                // We already handle int and float automatically, but not their Object
                // equivalents
                mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                        (mValueType == Float.class) ? sFloatEvaluator :
                        null;
            }
            if (mEvaluator != null) {
                // KeyframeSet knows how to evaluate the common types - only give it a custom
                // evaluator if one has been set on this class
                mKeyframes.setEvaluator(mEvaluator);
            }
        }
    

    這里mEvaluator進(jìn)行了三目運(yùn)算振峻,由于前面我們執(zhí)行的是ObjectAnimator.ofFloat(),所以mEvaluator就是sFloatEvaluator择份,這里就涉及到了估值器

        private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
    
        public class FloatEvaluator implements TypeEvaluator<Number> {
            public Float evaluate(float fraction, Number startValue, Number endValue) {
                float startFloat = startValue.floatValue();
                return startFloat + fraction * (endValue.floatValue() - startFloat);
            }
        }
    

    init()方法的目的是初始化PropertyValuesHolder扣孟,初始化的時候,會確定具體的估值器荣赶,這個float類型的估值器只有一個evaluate()方法凤价,返回線性插入起始值和結(jié)束值的結(jié)果,就是根據(jù)時間的變化規(guī)律計算得到每一步的運(yùn)算結(jié)果拔创。關(guān)于估值器和插值器的相關(guān)文章

    這里需要先說一下mKeyframes怎么來的呢利诺,正是前面提到的setIntValues方法中執(zhí)行的KeyframeSet.ofInt(values)。

       public void setIntValues(int... values) {
            mValueType = int.class;
            mKeyframes = KeyframeSet.ofInt(values);
        }
    
  • setCurrentPlayTime():

        public void setCurrentPlayTime(long playTime) {
            float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
            setCurrentFraction(fraction);
        }
    
       public void setCurrentFraction(float fraction) {
            initAnimation();
            fraction = clampFraction(fraction);
            mStartTimeCommitted = true; // do not allow start time to be compensated for jank
            if (isPulsingInternal()) {
                long seekTime = (long) (getScaledDuration() * fraction);
                long currentTime = AnimationUtils.currentAnimationTimeMillis();
                // Only modify the start time when the animation is running. Seek fraction will ensure
                // non-running animations skip to the correct start time.
                mStartTime = currentTime - seekTime;
            } else {
                // If the animation loop hasn't started, or during start delay, the startTime will be
                // adjusted once the delay has passed based on seek fraction.
                mSeekFraction = fraction;
            }
            mOverallFraction = fraction;
            final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
            animateValue(currentIterationFraction);
        }
    

    setCurrentFraction()方法基本都是時間的計算剩燥,最后執(zhí)行了animateValue()方法慢逾,這里先貼上ObjectAnimator類的該方法:

      void animateValue(float fraction) {
            final Object target = getTarget();
            ...
            super.animateValue(fraction);
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].setAnimatedValue(target);
            }
        }
    

在ObjectAnimator類的animateValue()方法中,需要注意

  • super.animateValue(fraction);即執(zhí)行父類的animateValue(),

       void animateValue(float fraction) {
          fraction = mInterpolator.getInterpolation(fraction);
          mCurrentFraction = fraction;
          int numValues = mValues.length;
          for (int i = 0; i < numValues; ++i) {
              mValues[i].calculateValue(fraction);
          }
          if (mUpdateListeners != null) {
              int numListeners = mUpdateListeners.size();
              for (int i = 0; i < numListeners; ++i) {
                  mUpdateListeners.get(i).onAnimationUpdate(this);
              }
          }
      }
    

    mInterpolator.getInterpolation(fraction);是獲取時間插值器侣滩,
    mValues[i].calculateValue(fraction);是將時間插值送給估值器口注,計算出 values

  • mValues[i].setAnimatedValue(target);

      void setAnimatedValue(Object target) {
          if (mProperty != null) {
              mProperty.set(target, getAnimatedValue());
          }
          if (mSetter != null) {
              try {
                  mTmpValueArray[0] = getAnimatedValue();
                  mSetter.invoke(target, mTmpValueArray);
              } catch (InvocationTargetException e) {
                  Log.e("PropertyValuesHolder", e.toString());
              } catch (IllegalAccessException e) {
                  Log.e("PropertyValuesHolder", e.toString());
              }
          }
      }
    

    注意,這個setAnimatedValue()方法是PropertyValuesHolder類的君珠,可以看到mSetter.invoke(target, mTmpValueArray)這行代碼通過反射進(jìn)行了屬性值的修改寝志。
    也就是說setCurrentPlayTime()方法的目的1,是獲取時間插值器值和估值器策添,2材部,改變target的屬性

至此,動畫的第一幀就執(zhí)行完畢了舰攒。


我們通過ObjectAnimator.ofFloat()方法败富,查看了跟蹤查看了整個屬性動畫的機(jī)制。這里貼出動畫機(jī)制的相關(guān)方法摩窃,由于簡書的這個圖片壓縮的太狠兽叮,最后只有這張圖勉強(qiáng)能看清(建議右鍵,在新頁面打開圖片):


ValueAnimator.png

我們得出一些結(jié)論:

  • 屬性動畫和我們生活中的動畫一樣猾愿,都是由一幀一幀構(gòu)成的鹦聪。
  • 屬性動畫需要優(yōu)先計算出開始幀和結(jié)束幀。
  • 屬性動畫通過start調(diào)用執(zhí)行動畫蒂秘,背后會進(jìn)行一系列的工作泽本。
    • 屬性動畫依靠監(jiān)聽 Choreographer使得其可以不斷地調(diào)用 doAnimationFrame() 來驅(qū)動動畫執(zhí)行每一個關(guān)鍵幀。
    • 每一次的 doAnimationFrame() 調(diào)用都會去計算時間插值姻僧,而通過時間插值器計算得到 fraction 又會傳給估值器规丽,使得估值器可以計算出屬性的當(dāng)前值。
    • PropertyValuesHolder作為屬性動畫的變量封裝和管理和以及通過反射修改目標(biāo)屬性的值撇贺。

當(dāng)然赌莺,從簡單的角度來說動畫機(jī)制就是如此這般了,但是這只是粗顆粒而言松嘶,上文中海油很多細(xì)節(jié)并沒有展開艘狭,比如如何保證動畫唯一性、動畫的相關(guān)時間是如何計算的翠订、比如插值器和估值器是怎么工作的巢音、Choreographer又是如何不斷調(diào)用的等等問題,后續(xù)我會根據(jù)時間情況慢慢梳理出來尽超。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末官撼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子橙弱,更是在濱河造成了極大的恐慌歧寺,老刑警劉巖燥狰,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異斜筐,居然都是意外死亡龙致,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門顷链,熙熙樓的掌柜王于貴愁眉苦臉地迎上來目代,“玉大人,你說我怎么就攤上這事嗤练¢涣耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵煞抬,是天一觀的道長霜大。 經(jīng)常有香客問我,道長革答,這世上最難降的妖魔是什么战坤? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮残拐,結(jié)果婚禮上途茫,老公的妹妹穿的比我還像新娘。我一直安慰自己溪食,他們只是感情好囊卜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著错沃,像睡著了一般栅组。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上枢析,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天笑窜,我揣著相機(jī)與錄音,去河邊找鬼登疗。 笑死,一個胖子當(dāng)著我的面吹牛嫌蚤,可吹牛的內(nèi)容都是我干的辐益。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼脱吱,長吁一口氣:“原來是場噩夢啊……” “哼智政!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起箱蝠,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤续捂,失蹤者是張志新(化名)和其女友劉穎垦垂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牙瓢,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劫拗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了矾克。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片页慷。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖胁附,靈堂內(nèi)的尸體忽然破棺而出酒繁,到底是詐尸還是另有隱情,我是刑警寧澤控妻,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布州袒,位于F島的核電站,受9級特大地震影響弓候,放射性物質(zhì)發(fā)生泄漏郎哭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一弓叛、第九天 我趴在偏房一處隱蔽的房頂上張望彰居。 院中可真熱鬧,春花似錦撰筷、人聲如沸陈惰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抬闯。三九已至,卻和暖如春关筒,著一層夾襖步出監(jiān)牢的瞬間溶握,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工蒸播, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睡榆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓袍榆,卻偏偏與公主長得像胀屿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子包雀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355