自定義View:Android歌詞控件

TicktockMusic 音樂播放器項(xiàng)目相關(guān)文章匯總:

簡(jiǎn)介

之前做 TicktockMusic 音樂播放器隙畜,一個(gè)必要的需求肯定是歌詞鸟蜡,在 github 上找了幾個(gè),發(fā)現(xiàn)或多或少都有點(diǎn)不滿足需求,所以就自己動(dòng)手寫了一個(gè),本篇文章主要介紹下實(shí)現(xiàn)的原理。

先附上項(xiàng)目地址和效果圖:

地址:https://github.com/Lauzy/LyricView

效果圖:


image

需求

歌詞的需求我想大家都很清楚,簡(jiǎn)單的話,直接打開一個(gè)音樂播放器查看一下撇眯。我們打開后分析一下歌詞的功能:歌詞完整的顯示出來报嵌、當(dāng)前歌詞變色、可以根據(jù)時(shí)間而進(jìn)行定位熊榛、可以手動(dòng)滑動(dòng)锚国、滑動(dòng)后顯示一個(gè)指示器、點(diǎn)擊指示器播放進(jìn)度跳轉(zhuǎn)玄坦、滑動(dòng)時(shí)指示器變色等等血筑。OK,我們自己寫歌詞控件煎楣,這些功能也是必不可少的豺总,接下來就逐步分析下實(shí)現(xiàn)的過程。

實(shí)現(xiàn)

  • 歌詞解析
  • 歌詞顯示
  • 滑動(dòng)處理
  • 指示器

基本實(shí)現(xiàn)就是這幾個(gè)過程择懂,接下來一步步的分析喻喳。

歌詞解析

首先,我們?cè)诰W(wǎng)上下載一個(gè)歌詞困曙,即以 lrc 為后綴的文件表伦。比如海闊天空這首歌的歌詞,我們用記事本或者其他工具打開后就可以看到具體的歌詞內(nèi)容慷丽,如下:


[ti: 海闊天空]
[ar:黃家駒]
[al:樂與怒]
[by:mp3.50004.com]
[00:00.00]Beyond:海闊天空 
[01:40.00][00:16.00]今天我寒夜里看雪飄過 
[01:48.00][00:24.00]懷著冷卻了的心窩飄遠(yuǎn)方 
[01:53.00][00:29.00]風(fēng)雨里追趕 
...

[00:42.00]多少次迎著冷眼與嘲笑 
[00:49.00]從沒有放棄過心中的理想 
[00:54.00]一剎那恍惚 
...

可以看到蹦哼,歌詞主要包含歌名、歌手盈魁、專輯翔怎、作者等頭元素窃诉,以及歌詞的主體內(nèi)容杨耙,我們需要處理的就是主體的歌詞內(nèi)容。首先飘痛,歌詞是一行一行的文本珊膜,其次,每行的文本都包含時(shí)間標(biāo)簽和具體的一行歌詞宣脉,我們首先將歌詞解析為一行行的數(shù)據(jù)车柠。


        InputStreamReader isr = null;
        BufferedReader br = null;
        try {
            isr = new InputStreamReader(inputStream, CHARSET);
            br = new BufferedReader(isr);
            String line;
            while ((line = br.readLine()) != null) {
                //此處的 line 即為一行行的文本
                //parseLrc 方法為解析單行
                List<Lrc> lrcList = parseLrc(line);
                if (lrcList != null && lrcList.size() != 0) {
                    lrcs.addAll(lrcList);
                }
            }
            sortLrcs(lrcs);
            return lrcs;
        }catch ...

解析為一行行的文字后,就需要具體的處理單行的文字了塑猖,我們可以看到竹祷,大部分歌詞包含兩種格式,即單個(gè)時(shí)間標(biāo)簽和多個(gè)時(shí)間標(biāo)簽羊苟,這里可以采用正則表達(dá)式來匹配文字塑陵,正則表達(dá)式為 (([\d{2}:\d{2}.\d{2}])+)(.*)


[01:53.00][00:29.00]風(fēng)雨里追趕    //多個(gè)時(shí)間標(biāo)簽

[00:42.00]多少次迎著冷眼與嘲笑     //單個(gè)時(shí)間標(biāo)簽

接下來根據(jù)正則表達(dá)式來解析單行歌詞


    private static List<Lrc> parseLrc(String lrcLine) {
        if (lrcLine.trim().isEmpty()) {
            return null;
        }
        List<Lrc> lrcs = new ArrayList<>();
        Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine);
        if (!matcher.matches()) {
            return null;
        }

        String time = matcher.group(1);
        String content = matcher.group(3);
        Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time);

        while (timeMatcher.find()) {
            String min = timeMatcher.group(1);
            String sec = timeMatcher.group(2);
            String mil = timeMatcher.group(3);
            Lrc lrc = new Lrc();
            if (content != null && content.length() != 0) {
                lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000
                        + Long.parseLong(mil) * 10);
                lrc.setText(content);
                lrcs.add(lrc);
            }
        }
        return lrcs;
    }

這樣,第一步就完成了蜡励,歌詞解析完成后得到歌詞的數(shù)據(jù)集合令花,每個(gè)元素都包括時(shí)間和內(nèi)容阻桅。

歌詞顯示

歌詞顯示的思路就是將歌詞一行行的畫出來,我們首先假設(shè)屏幕足夠大兼都,那么只需要定位第一行歌詞的位置嫂沉,畫出來第一行歌詞,然后逐行下移一個(gè)固定的距離扮碧,再畫出下一行歌詞趟章,依次類推,整個(gè)歌詞內(nèi)容就會(huì)全部畫在畫布上了慎王。依照這個(gè)思路尤揣,我們可以先畫出來文字。


      //此處為偽代碼

      float y = getLrcHeight() / 2;
      float x = getLrcWidth() / 2 + getPaddingLeft();
      for (int i = 0; i < getLrcCount(); i++) {
            if (i > 0) {
                y += textHeight  + mLrcLineSpaceHeight;
            }
          ...
           canvas.drawText(text, x, y, mPaint);
       }

畫出來文字的思路就是這樣柬祠,首先從屏幕的中間開始北戏,然后縱坐標(biāo)每次增加文字的高度與距離之和,依次畫出來每行文字漫蛔。這樣嗜愈,假如屏幕足夠大的話,那么所有的歌詞就會(huì)從屏幕中間開始莽龟,依次向下一行行的顯示出來蠕嫁。但是,我們的屏幕不可能是無限大的毯盈。首先剃毒,假如一行歌詞很長(zhǎng)的話,canvas.drawText() 的效果會(huì)是屏幕覆蓋掉多余的 text 文字搂赋,所以當(dāng)一行文字超過我們?cè)O(shè)置的 View 最大寬度時(shí)赘阀,最理想的方法就是多余的部分換行,就像 TextView 一樣脑奠。所幸的是基公,Android 中給我們提供了方法,那就是 StaticLayout 宋欺,StaticLayout 用法很簡(jiǎn)單轰豆,我們使用它來替代 canvas.drawText(),下面是基本用法齿诞。


    private void drawLrc(Canvas canvas, float x, float y, int i) {
        mTextPaint.setTextSize(mLrcTextSize);
        String text = mLrcData.get(i).getText();
        StaticLayout staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),
                    Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);   
        canvas.save();
        canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);
        staticLayout.draw(canvas);
        canvas.restore();
    }

這樣我們就能獲取想要的效果了酸休,文字一行行的排列,文字比較長(zhǎng)的話祷杈,會(huì)自動(dòng)換行到下一行斑司。但是,這樣僅僅是實(shí)現(xiàn)效果吠式,在 onDraw() 方法中陡厘,我們應(yīng)該盡量的避免新建對(duì)象抽米,以免造成界面的卡頓,而 StaticLayout 需要實(shí)例化對(duì)象糙置,所以這邊需要我們手動(dòng)優(yōu)化一下云茸。
因?yàn)槭褂?StaticLayout 后,一行文字的高度不再固定谤饭,所以 y 坐標(biāo)不再累加固定的文字高度标捺,而是上一行和下一行文字之和的一半+文字間距。代碼如下:


     for (int i = 0; i < getLrcCount(); i++) {
            if (i > 0) {
                y += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;
            }
            drawLrc(canvas, x, y, i);
        }

為了避免過多的實(shí)例化揉抵,在使用 StaticLayout 時(shí)亡容,這里采用 map 進(jìn)行緩存,創(chuàng)建過對(duì)象后緩存起來冤今,后邊就不需要再繼續(xù)創(chuàng)建闺兢。


   private void drawLrc(Canvas canvas, float x, float y, int i) {
        String text = mLrcData.get(i).getText();
        StaticLayout staticLayout = mLrcMap.get(text);
        if (staticLayout == null) {
            mTextPaint.setTextSize(mLrcTextSize);
            staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),
                    Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
            mLrcMap.put(text, staticLayout);
        }
        canvas.save();
        canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);
        staticLayout.draw(canvas);
        canvas.restore();
    }

到這里,我們已經(jīng)解決了水平方向的顯示戏罢,但是垂直方向呢屋谭,垂直方向則利用滑動(dòng)來解決,這也是歌詞的基本需求之一龟糕。

滑動(dòng)處理

歌詞的滑動(dòng)是做歌詞控件的必然要求桐磁,包括根據(jù)音樂播放的進(jìn)度進(jìn)行自動(dòng)的滑動(dòng),以及用戶主動(dòng)拖動(dòng)的滑動(dòng)讲岁,我們來逐個(gè)分析我擂。

1、根據(jù)播放進(jìn)度滾動(dòng)

音樂的播放時(shí)間進(jìn)度可以根據(jù) MediaPlayer 來獲取缓艳,在一首音樂播放的過程中校摩,播放的進(jìn)度是不斷更新的,所以就需要我們根據(jù)這個(gè)不斷更新的時(shí)間郎任,來決定歌詞滾動(dòng)的位置秧耗。

我們需要比較不斷更新的時(shí)間和每行歌詞的時(shí)間备籽,最接近或者相等時(shí)舶治,就可以視作音樂播放的進(jìn)度對(duì)應(yīng)當(dāng)前這一行歌詞,所以需要獲取播放時(shí)間對(duì)應(yīng)的歌詞行數(shù)车猬。

    private int getUpdateTimeLinePosition(long time) {
        int linePos = 0;
        for (int i = 0; i < getLrcCount(); i++) {
            Lrc lrc = mLrcData.get(i);
            if (time >= lrc.getTime()) {
                if (i == getLrcCount() - 1) {假如時(shí)間大于最后一行歌詞的時(shí)間霉猛,則行數(shù)為最后一行
                    linePos = getLrcCount() - 1;
                } else if (time < mLrcData.get(i + 1).getTime()) {//否則若同時(shí)小于下一行,則行數(shù)為 i
                    linePos = i;
                    break;
                }
            }
        }
        return linePos;
    }

獲取行數(shù)之后珠闰,行數(shù)變化時(shí)惜浅,就可以利用動(dòng)畫,來讓歌詞進(jìn)行滾動(dòng)伏嗜。

     private void scrollToPosition(int linePosition) {
        float scrollY = getItemOffsetY(linePosition);//將要滾動(dòng)的一行的偏移量
        final ValueAnimator animator = ValueAnimator.ofFloat(mOffset, scrollY);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffset = (float) animation.getAnimatedValue();
                invalidateView();
            }
        });
        animator.setDuration(300);
        animator.start();
    }

此處最重要的屬性就是 mOffset 坛悉,mOffset 是為了決定歌詞偏移量而定義的一個(gè)屬性伐厌, mOffset 的取值是在原有值和目標(biāo)行的偏移量之間,由動(dòng)畫控制其變化裸影。假如向下滑動(dòng)挣轨,初始為0,則滾動(dòng)到第二行歌詞轩猩,mOffset 就是從 0 到 getItemOffsetY(1) 的過程卷扮。 getItemOffsetY(i) 就是第 i 行的偏移量。

   private float getItemOffsetY(int linePosition) {
        float tempY = 0;
        for (int i = 1; i <= linePosition; i++) {
            tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;
        }
        return tempY;
    }

然后均践,再根據(jù)播放進(jìn)度晤锹,進(jìn)行不斷的更新。

    public void updateTime(long time) {
        if (isLrcEmpty()) {
            return;
        }
        int linePosition = getUpdateTimeLinePosition(time);
        if (mCurrentLine != linePosition) {
            mCurrentLine = linePosition;
            ViewCompat.postOnAnimation(LrcView.this, mScrollRunnable);
        }
    }

    private Runnable mScrollRunnable = new Runnable() {
        @Override
        public void run() {
            scrollToPosition(mCurrentLine);
        }
    };

到此為止彤委,我們已經(jīng)完成了歌詞的自動(dòng)滾動(dòng)功能鞭铆。

2、滑動(dòng)事件處理

僅僅有自動(dòng)滾動(dòng)是無法滿足歌詞的需求的焦影,所以我們還需要控制歌詞的滑動(dòng)事件衔彻,讓用戶可以手動(dòng)滑動(dòng)歌詞到某個(gè)位置。既然是手勢(shì)的事件偷办,那么就需要我們重寫 onTouch 方法艰额,處理不同的手勢(shì)。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isLrcEmpty()) { //歌詞為空椒涯,則默認(rèn)事件
            return super.onTouchEvent(event);
        }
        //速度跟蹤
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                removeCallbacks(mScrollRunnable);
                if (!mOverScroller.isFinished()) {
                    mOverScroller.abortAnimation();
                }
                mLastMotionX = event.getX();
                mLastMotionY = event.getY();
                isUserScroll = true;
                isDragging = false;
                break;

            case MotionEvent.ACTION_MOVE:
                float moveY = event.getY() - mLastMotionY;
                if (Math.abs(moveY) > mScaledTouchSlop) {
                    isDragging = true;
                    isShowTimeIndicator = isEnableShowIndicator;
                }
                if (isDragging) {
                    float maxHeight = getItemOffsetY(getLrcCount() - 1);
                    if (mOffset < 0 || mOffset > maxHeight) {
                        moveY /= 3.5f;
                    }
                    mOffset -= moveY;
                    mLastMotionY = event.getY();
                    invalidateView();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                handleActionUp(event);
                break;
        }
        return true;
    }

簡(jiǎn)單解釋下上述代碼柄沮,先忽略掉 VelocityTracker 和 OverScroller。在 ACTION_DOWN 時(shí)废岂,記錄下 x 和 y 的坐標(biāo)祖搓;然后在 ACTION_MOVE 時(shí),若拖動(dòng)的距離大于觸發(fā)滑動(dòng)的最小值湖苞,則改變 mOffset 的值拯欧,然后刷新 View。當(dāng) mOffset < 0 或者 mOffset > maxHeight 即歌詞已經(jīng)滾動(dòng)到頂部或者底部時(shí)财骨,為了回彈的阻尼效果镐作,將 moveY 的值大幅減小。
接下來介紹下手勢(shì)抬起的事件隆箩,VelocityTracker 和 OverScroller 就是用于此處该贾,在手勢(shì)滑動(dòng)抬起時(shí),我們希望有一個(gè) fling 的效果捌臊,Android 中的 OverScroller 可以簡(jiǎn)單的實(shí)現(xiàn)這種效果杨蛋。


 private void handleActionUp(MotionEvent event) {
       
        //越界的處理
        if (overScrolled() && mOffset < 0) { 
            scrollToPosition(0);
            ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
            return;
        }

        if (overScrolled() && mOffset > getItemOffsetY(getLrcCount() - 1)) {
            scrollToPosition(getLrcCount() - 1);
            ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
            return;
        }

        mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
        float YVelocity = mVelocityTracker.getYVelocity();
        float absYVelocity = Math.abs(YVelocity);
        if (absYVelocity > mMinimumFlingVelocity) {
            mOverScroller.fling(0, (int) mOffset, 0, (int) (-YVelocity), 0,
                    0, 0, (int) getItemOffsetY(getLrcCount() - 1),
                    0, (int) getTextHeight(0));
            invalidateView();
        }
        releaseVelocityTracker();
        if (isAutoAdjustPosition) {
            ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
        }
    }

當(dāng)手勢(shì)抬起時(shí),計(jì)算下當(dāng)前的手勢(shì)速度,然后利用 mOverScroller.fling() 方法逞力,在 computeScroll() 中改變 mOffset 的值即可曙寡。

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mOverScroller.computeScrollOffset()) {
            mOffset = mOverScroller.getCurrY();
            invalidateView();
        }
    }

這樣,主動(dòng)的手勢(shì)功能也已經(jīng)實(shí)現(xiàn)了寇荧。

指示器

用戶手動(dòng)滑動(dòng)歌詞的目的卵皂,很大一部分是為了滑動(dòng)后能根據(jù)歌詞來控制播放的進(jìn)度,所以指示器也是一個(gè)不可或缺的功能砚亭。當(dāng)用戶滑動(dòng)歌詞時(shí)灯变,顯示指示器,歌詞經(jīng)過指示器的位置時(shí)變色捅膘,用戶點(diǎn)擊指示器按鈕后添祸,歌詞跳轉(zhuǎn)到這個(gè)位置,播放進(jìn)度也到了這里寻仗。
首先要做的就是顯示指示器以及歌詞變色刃泌,這里就需要我們獲取歌詞在指示器的位置時(shí),歌詞的行數(shù)署尤,因?yàn)橹甘酒鳟嬙诟柙~的中間位置耙替,所以某一行歌詞的偏移量和 mOffset 的差值最小時(shí),就可以看作這一行歌詞經(jīng)過了指示器曹体。

public int getIndicatePosition() {
        int pos = 0;
        float min = Float.MAX_VALUE;
        //itemOffset 和 mOffset 最小時(shí)俗扇,當(dāng)前的位置
        for (int i = 0; i < mLrcData.size(); i++) {
            float offsetY = getItemOffsetY(i);
            float abs = Math.abs(offsetY - mOffset);
            if (abs < min) {
                min = abs;
                pos = i;
            }
        }
        return pos;
    }

然后在 onDraw() 中,畫出來具體的特性箕别。

       if (isShowTimeIndicator) {
            mPlayDrawable.draw(canvas); // 畫出指示器的播放按鈕
            long time = mLrcData.get(indicatePosition).getTime();
            float timeWidth = mIndicatorPaint.measureText(LrcHelper.formatTime(time)); //獲取指示時(shí)間的文字長(zhǎng)度
            mIndicatorPaint.setColor(mIndicatorLineColor);
            // 畫出指示線
            canvas.drawLine(mPlayRect.right + mIconLineGap, getHeight() / 2, 
                    getWidth() - timeWidth * 1.3f, getHeight() / 2, mIndicatorPaint);
            int baseX = (int) (getWidth() - timeWidth * 1.1f);
            float baseline = getHeight() / 2 - (mIndicatorPaint.descent() - mIndicatorPaint.ascent()) / 2 - mIndicatorPaint.ascent();
            mIndicatorPaint.setColor(mIndicatorTextColor);
            //畫出指示時(shí)間文字
            canvas.drawText(LrcHelper.formatTime(time), baseX, baseline, mIndicatorPaint);
        }

最后铜幽,處理用戶點(diǎn)擊事件,并且將當(dāng)前行的歌詞及時(shí)間進(jìn)行回調(diào)串稀,來控制播放進(jìn)度除抛。

if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) {
            isShowTimeIndicator = false;
            invalidateView();
            if (mOnPlayIndicatorLineListener != null) {
                mOnPlayIndicatorLineListener.onPlay(mLrcData.get(getIndicatePosition()).getTime(),
                        mLrcData.get(getIndicatePosition()).getText());
            }
        }
//點(diǎn)擊在按鈕范圍才響應(yīng)
private boolean onClickPlayButton(MotionEvent event) {
        float left = mPlayRect.left;
        float right = mPlayRect.right;
        float top = mPlayRect.top;
        float bottom = mPlayRect.bottom;
        float x = event.getX();
        float y = event.getY();
        return mLastMotionX > left && mLastMotionX < right && mLastMotionY > top
                && mLastMotionY < bottom && x > left && x < right && y > top && y < bottom;
    }

這樣,指示器的功能也就完成了母截。

總結(jié)

上述就是整個(gè)歌詞控件繪制的流程到忽,還有一些顏色變化等細(xì)節(jié)功能就不一一說明了,有興趣可以看一看源碼清寇。這個(gè)控件我也已經(jīng)封裝成了一個(gè)自定義 View 的庫喘漏,可以在 https://github.com/Lauzy/LyricView 這里看下具體的使用。歡迎討論颗管、歡迎 star陷遮。

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市垦江,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖比吭,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绽族,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡衩藤,警方通過查閱死者的電腦和手機(jī)吧慢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赏表,“玉大人检诗,你說我怎么就攤上這事∑敖耍” “怎么了逢慌?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)间狂。 經(jīng)常有香客問我攻泼,道長(zhǎng),這世上最難降的妖魔是什么鉴象? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任忙菠,我火速辦了婚禮,結(jié)果婚禮上纺弊,老公的妹妹穿的比我還像新娘牛欢。我一直安慰自己,他們只是感情好淆游,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布氢惋。 她就那樣靜靜地躺著,像睡著了一般稽犁。 火紅的嫁衣襯著肌膚如雪焰望。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天已亥,我揣著相機(jī)與錄音熊赖,去河邊找鬼。 笑死虑椎,一個(gè)胖子當(dāng)著我的面吹牛震鹉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捆姜,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼传趾,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了泥技?” 一聲冷哼從身側(cè)響起浆兰,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后簸呈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榕订,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年蜕便,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劫恒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡轿腺,死狀恐怖两嘴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情族壳,我是刑警寧澤憔辫,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站决侈,受9級(jí)特大地震影響螺垢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赖歌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一枉圃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧庐冯,春花似錦孽亲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至栖茉,卻和暖如春篮绿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吕漂。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工亲配, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惶凝。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓吼虎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親苍鲜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子思灰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容