自定义View实战-仿京东首页轮播文字(又名垂直跑马灯)

自定义View实战-仿京东首页轮播文字(又名垂直跑马灯)

京东客户端的轮播文字效果:

这里写图片描述

本次要实现的只是后面滚动的文字(前面的用ImageView或者TextView实现即可),看一下实现的效果:

这里写图片描述

经过更改后本组件已可以已开源库的形式添加到项目中使用(不用下载然后导入),使用方法及介绍详见Github.本文的实例Demo也在这里.ADTextView,欢迎star.

我还写了另外一个开源库,多达288种动画效果定制的侧滑菜单库有兴趣的点了看一下,欢迎star(不应该说欢迎,应该说求star,因为快毕业找工作了,多几个star简历上也好看一下,谢谢了)

关于开源库的详细介绍可以看这篇博客:多达288种动态效果的侧滑菜单开源库,满足您项目的各种需求

关于如何发布一个开源库的内容可以查看这篇博客:发布新手的第一个开源库-快速发布开源库到JitPack

好了,接着说垂直跑马灯的内容

实现思路:

这里写图片描述
这里写图片描述

上图只是一个大概的思路,要实现还需要完善更多的细节,下面会一步步的来实现这个效果:

1.封装数据源:从图上可以看到,轮播的文字是分为两个部分的,暂且把它们分别叫做前缀和内容,而且实际的使用过程中点击轮播图肯定是需要跳转页面的,而且大部分应该是WebView,不妨我们就设置点击时候需要获取的内容就是一个链接,那么数据源的结构就很明了了

这里写图片描述

创建ADEnity类并设计参数完善一些基本的方法,代码如下

public class ADEnity {
    private String mFront ; //前面的文字
    private String mBack ; //后面的文字
    private String mUrl ;//包含的链接

    public ADEnity(String mFront, String mBack,String mUrl) {
        this.mFront = mFront;
        this.mBack = mBack;
        this.mUrl = mUrl;
    }

    public String getmUrl() {
        return mUrl;
    }

    public void setmUrl(String mUrl) {
        this.mUrl = mUrl;
    }

    public String getmFront() {
        return mFront;
    }

    public void setmFront(String mFront) {
        this.mFront = mFront;
    }

    public String getmBack() {
        return mBack;
    }

    public void setmBack(String mBack) {
        this.mBack = mBack;
    }
}

2.接下来应该是定制这个自定义View了,首先理一下思路,看一个构造图

这里写图片描述

实现这个自定义View的所有参数都在上表列出了,大部分参数很容易理解,个别参数加进去是很有必要的,比如说是否初始化进入文字的纵坐标,文字是否在移动中,是否处于停顿状态,这三个参数,之后的内容会详细的叙述一下.

在动手绘制之前还得需要知道一点基础的知识,就是关于绘制文字的方法,里面有很多细节需要处理

首先是画布绘制文字的方法:

返回值 方法 描述
void drawText(String text, float x, float y, Paint paint) Draw the text, with origin at (x,y), using the specified paint.
void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) Draw the specified range of text, specified by start/end, with its origin at (x,y), in the specified Paint.
void drawText(char[] text, int index, int count, float x, float y, Paint paint) Draw the text, with origin at (x,y), using the specified paint.
void drawText(String text, int start, int end, float x, float y, Paint paint) Draw the text, with origin at (x,y), using the specified paint.

方法都比较好理解,绘制指定字符串(可以指定范围)在坐标( x , y )处,但是其中的x,y并不是我们所理解的应该是文字左上角的坐标点.其中的x坐标是根据Paint的属性可变换的,默认的x是文字的左边坐标,如果Paint设置了paint.setTextAlign(Paint.Align.CENTER);那就是字符的中心位置.Y坐标是文字的baseliney坐标.

关于绘制文字的baseline:

用图来说话吧

这里写图片描述

图中蓝色的线即为baseline,可以看出他既不是顶部坐标也不是底部坐标,那么当我们绘制文字的时候肯定是希望能把文字绘制在正中间.这时候就要引入paint.getTextBound()方法了

getTextBounds(String text, int start, int end, Rect bounds),传入一个Rect对象,调用此方法之后则会填充这个rect对象,而填充的内容就是所绘制的文字相对于baseline的偏移坐标,将这个Rect加上baseline的坐标,绘制后是这样的:

这里写图片描述

但其实他的值只是(2,-25,76,3),是相对于baseline的位置,画个图会比较好理解

这里写图片描述

那么要将文字绘制在中间,那么实际绘制baseline的坐标应该是组件的中心,加上文字中心(即图中框的中间坐标)相对于baseline的偏移值

这里写图片描述

这张图中应该会好理解实际绘制文字的坐标与组件中心坐标的关系.关于偏移值的计算,按常规的几何计算方法,应该是组件的中心坐标+偏移值的绝对值==baseline坐标(即实际绘制的坐标),但是由于框的坐标值都是相对于baseline来计算的,top为负值,botton为正值,那么这个偏移值就可以直接用(top+bottom)/2的绝对值来表示,没看懂的同学可以画个草图,用top=-25,bottom=3来算一下,看是否结果是一致的.

经过上面的理解,那我们来绘制正确绘制文字的方法也就确定了

已获得组件的高度int mHeight , 文字外框Rect bound的情况下

绘制文字在正中间

mHeight  / 2 - (bound.top + bound.bottom) / 2
(因为bound.top + bound.bottom为负值,所以用减法)
//在纵坐标为mY的地方绘制文字
//计算方式
//mheight /2 = mY + (bound.top + bound.bottom) / 2 ;

文字滚动到最高点

mY == 0 - bound.bottom
//在纵坐标为mY的地方绘制,此时文字刚好移动到最高点
//计算方式
//mY + bound.bottom = 0 ;

文字滚动到最低点,刚好滚出组件

mY = mHeight  - indexBound.top;
//在纵坐标为mY的地方绘制,此时文字刚好移动到最高点
//计算方式
//mY + bound.top = mHeight  ;

知道了如何正确的绘制文字和边界情况的坐标判断,下面就到了绘制文字的步骤了

书写自定义View,定义需要用到的属性,完成构造方法

public class ADTextView extends View {
    private int mSpeed; //文字出现或消失的速度 建议1~5
    private int mInterval; //文字停留在中间的时长
    private int mFrontColor; //前缀颜色
    private int mContentColor; //内容的颜色
    private int mFrontTextSize; //前缀文字大小
    private int mContentTextSize; //内容文字大小

    private List<AdEntity> mTexts; //显示文字的数据源
    private int mY = 0; //文字的Y坐标
    private int mIndex = 0; //当前的数据下标
    private Paint mPaintContent; //绘制内容的画笔
    private Paint mPaintFront; //绘制前缀的画笔
    private boolean isMove = true; //文字是否移动
    private String TAG = "ADTextView";
    private boolean hasInit = false;
    private boolean isPaused = false;

       public ADTextView(Context context) {
        this(context, null);
    }

    public ADTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //获取资源属性值
        obtainStyledAttrs(attrs);
        //初始化数据
        init();
    }

定义资源文件属性,values下建立attrs.xml文件

<declare-styleable name="ADTextView">
        <!--文字进入与消失的时间-->
        <attr name="ad_text_view_speed" format="integer"/>

        <!--文字停留在中心的时间-->
        <attr name="ad_text_view_interval" format="integer"/>

        <!--前缀文字颜色-->
        <attr name="ad_text_front_color" format="color"/>
        <!--前缀文字大小-->
        <attr name="ad_text_front_size" format="dimension"/>

        <!--内容文字颜色-->
        <attr name="ad_text_content_color" format="color"/>
        <!--内容文字大小-->
        <attr name="ad_text_content_size" format="dimension"/>
    </declare-styleable>

代码中获取资源文件内的属性值,并赋予默认值

 //获取资源文件
    private void obtainStyledAttrs(AttributeSet attrs) {
        TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.ADTextView);
        mSpeed = array.getInt(R.styleable.ADTextView_ad_text_view_speed, 1);
        mInterval = array.getInt(R.styleable.ADTextView_ad_text_view_interval, 2000);
        mFrontColor = array.getColor(R.styleable.ADTextView_ad_text_front_color, Color.RED);
        mContentColor = array.getColor(R.styleable.ADTextView_ad_text_content_color, Color.BLACK);
        mFrontTextSize = (int) array.getDimension(R.styleable.ADTextView_ad_text_front_size, SizeUtil.Sp2Px(getContext(), 15));
        mContentTextSize = (int) array.getDimension(R.styleable.ADTextView_ad_text_content_size, SizeUtil.Sp2Px(getContext(), 15));
        array.recycle();
    }

注:设置默认值时用到了尺寸的转换,详情见这篇博客:自定义View之尺寸的转化

初始化数据

//初始化默认值
    private void init() {
        mIndex = 0;
        mPaintFront = new Paint();
        mPaintFront.setAntiAlias(true);
        mPaintFront.setDither(true);
        mPaintFront.setTextSize(mFrontTextSize);
        mPaintFront.setColor(mFrontColor);

        mPaintContent = new Paint();
        mPaintContent.setAntiAlias(true);
        mPaintContent.setDither(true);
        mPaintContent.setTextSize(mContentTextSize);
        mPaintContent.setColor(mContentColor);

    }

重写onMeasure进行宽高的测量(细节见注释)

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        //设置宽高
        setMeasuredDimension(width, height);
    }

    //测量宽度
    private int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            result = size; //具体的值
        } else { //高度至少为两倍字高
            int mfronTextHeight = (int) (mPaintFront.descent() - mPaintFront.ascent()); //前缀文字字高
            int mContentTextHeight = (int) (mPaintContent.descent() - mPaintContent.ascent()); //内容文字字高
            result = Math.max(mfronTextHeight, mContentTextHeight) * 2;

            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    //测量高度
    private int measureWidth(int widthMeasureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else { //宽度最小十个字的宽度
            String text = "十个字十个字十个字字";
            Rect rect = new Rect();
            mPaintContent.getTextBounds(text, 0, text.length(), rect);
            result = rect.right - rect.left;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

前面的叙述中我们知道,刚开始进入的时候文字应该是位于组件的底部的,但是这个值是需要获取组件的高度和当前显示文字的情况下来判断的,所以应该放在onDraw内来初始化这个值,所以需要前面的是否初始化的属性,判断当mY==0并且未初始化的时候给mY赋值.

接下来就是onDraw内的处理

获取当前的数据

            ADEnity model = mTexts.get(mIndex);
            String font = model.getmFront();
            String back = model.getmBack();
           

为测量前缀与内容的宽度,获取文字的Rect对象

 //前缀的Bound
            Rect indexBound = new Rect();
            mPaintFront.getTextBounds(font, 0, font.length(), indexBound);

            //内容文字的Bound
            Rect contentBound = new Rect();
            mPaintContent.getTextBounds(back, 0, back.length(), contentBound);
            if (mY == 0 && hasInit == false) {
                mY = getMeasuredHeight() - indexBound.top;
                hasInit = true;
            }

mY进行初始化

if (mY == 0 && hasInit == false) {
                mY = getMeasuredHeight() - indexBound.top;
                hasInit = true;
            }

绘制文字

  canvas.drawText(back, 0, back.length(), (indexBound.right - indexBound.left) + 20, mY, mPaintContent);
            canvas.drawText(font, 0, font.length(), 10, mY, mPaintFront);

对边界情况的处理

//移动到最上面
if (mY <= 0 - indexBound.bottom) {
mY = getMeasuredHeight() - indexBound.top; //返回底部
mIndex++; //换下一组数据
isPaused = false; //重置暂停状态
        }

 //移动到中间
            if (!isPaused && mY <= getMeasuredHeight() / 2 - (indexBound.top + indexBound.bottom) / 2) {
                isMove = false;
                isPaused = true;
                Timer timer = new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        postInvalidate();
                        isMove = true;
                    }
                }, mInterval);
            }
            mY -= mSpeed;

移动的处理与数据源的处理

mY -= mSpeed; //速度即为每次移动的像素值,推荐1~5,这也是前面判断中间与最上方的时候使用<=的原因,如果每次只移动1像素,使用==完全可以,其他值则有可能跳过==的这个条件,导致不会停顿或者不会循环
//循环使用数据
if (mIndex == mTexts.size()) {
     mIndex = 0;
}
//如果是处于移动状态时的,则延迟绘制
//计算公式为一个比例,一个时间间隔移动组件高度,则多少毫秒来移动1像素
if (isMove) {
  postInvalidateDelayed(mDuration / getMeasuredHeight());
}

至此对逻辑的处理就完成了,接下来要设置点击事件

//设置一个回调并设置setXXX方法
public interface onClickLitener {
        public void onClick(String mUrl);
}

private onClickLitener onClickLitener;

public void setOnClickLitener(TextViewAd.onClickLitener onClickLitener) {
        this.onClickLitener = onClickLitener;
}

//重写onTouchEvent事件,并且要返回true,表明当前的点击事件由这个组件自身来处理
@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (onClickLitener != null) {
         //调用回调,将当前数据源的链接传出去         onClickLitener.onClick(mTexts.get(mIndex).getmUrl());
                }

                break;
        }
        return true;
    }

暴露一些其他属性的设置方式

 //设置数据源
    public void setmTexts(List<AdEntity> mTexts) {
        this.mTexts = mTexts;
    }

    //设置广告文字的停顿时间
    public void setInterval(int mInterval) {
        this.mInterval = mInterval;
    }

    //设置速度
    public void setSpeed(int spedd) {
        this.mSpeed = spedd;
    }

    //设置前缀的文字颜色
    public void setFrontColor(int mFrontColor) {
        mPaintFront.setColor(mFrontColor);
    }

    //设置正文内容的颜色
    public void setBackColor(int mBackColor) {
        mPaintContent.setColor(mBackColor);
    }

有兴趣的同学可以将这些属性设置到attrs.xml文件中然后就可以在布局文件中设置属性了,这里就不演示了,因为觉得每次copy这个View还得把xml文件也copy比较麻烦,毕竟as有自动补全,可以很方便的看到暴露在外面的方法.(个人感受而已).

贴一下完整的ADTextView的代码,方便查看

package com.brioal.brioallib.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.brioal.brioallib.R;
import com.brioal.brioallib.entity.AdEntity;
import com.brioal.baselib.util.klog.KLog;
import com.brioal.baselib.util.SizeUtil;

import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 仿京东垂直滚动广告栏
 * Created by Brioal on 2016/7/22.
 */

public class ADTextView extends View {
    private int mSpeed; //文字出现或消失的速度 建议1~5
    private int mInterval; //文字停留在中间的时长
    private int mFrontColor; //前缀颜色
    private int mContentColor; //内容的颜色
    private int mFrontTextSize; //前缀文字大小
    private int mContentTextSize; //内容文字大小

    private List<AdEntity> mTexts; //显示文字的数据源
    private int mY = 0; //文字的Y坐标
    private int mIndex = 0; //当前的数据下标
    private Paint mPaintContent; //绘制内容的画笔
    private Paint mPaintFront; //绘制前缀的画笔
    private boolean isMove = true; //文字是否移动
    private String TAG = "ADTextView";
    private boolean hasInit = false;
    private boolean isPaused = false;

    public interface onClickListener {
        public void onClick(String mUrl);
    }

    private onClickListener onClickListener;

    public void setOnClickListener(onClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public ADTextView(Context context) {
        this(context, null);
    }

    public ADTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        obtainStyledAttrs(attrs);
        init();
    }

    //获取资源文件
    private void obtainStyledAttrs(AttributeSet attrs) {
        TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.ADTextView);
        mSpeed = array.getInt(R.styleable.ADTextView_ad_text_view_speed, 1);
        mInterval = array.getInt(R.styleable.ADTextView_ad_text_view_interval, 2000);
        mFrontColor = array.getColor(R.styleable.ADTextView_ad_text_front_color, Color.RED);
        mContentColor = array.getColor(R.styleable.ADTextView_ad_text_content_color, Color.BLACK);
        mFrontTextSize = (int) array.getDimension(R.styleable.ADTextView_ad_text_front_size, SizeUtil.Sp2Px(getContext(), 15));
        mContentTextSize = (int) array.getDimension(R.styleable.ADTextView_ad_text_content_size, SizeUtil.Sp2Px(getContext(), 15));
        array.recycle();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (onClickListener != null) {
                    onClickListener.onClick(mTexts.get(mIndex).getmUrl());
                }

                break;
        }
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    //测量宽度
    private int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else { //高度至少为两倍字高
            int mfronTextHeight = (int) (mPaintFront.descent() - mPaintFront.ascent()); //前缀文字字高
            int mContentTextHeight = (int) (mPaintContent.descent() - mPaintContent.ascent()); //内容文字字高
            result = Math.max(mfronTextHeight, mContentTextHeight) * 2;

            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    //测量高度
    private int measureWidth(int widthMeasureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else { //宽度最小十个字的宽度
            String text = "十个字十个字十个字字";
            Rect rect = new Rect();
            mPaintContent.getTextBounds(text, 0, text.length(), rect);
            result = rect.right - rect.left;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    //设置数据源
    public void setmTexts(List<AdEntity> mTexts) {
        this.mTexts = mTexts;
    }

    //设置广告文字的停顿时间
    public void setInterval(int mInterval) {
        this.mInterval = mInterval;
    }

    //设置速度
    public void setSpeed(int spedd) {
        this.mSpeed = spedd;
    }

    //设置前缀的文字颜色
    public void setFrontColor(int mFrontColor) {
        mPaintFront.setColor(mFrontColor);
    }

    //设置正文内容的颜色
    public void setBackColor(int mBackColor) {
        mPaintContent.setColor(mBackColor);
    }

    //初始化默认值
    private void init() {
        mIndex = 0;
        mPaintFront = new Paint();
        mPaintFront.setAntiAlias(true);
        mPaintFront.setDither(true);
        mPaintFront.setTextSize(mFrontTextSize);
        mPaintFront.setColor(mFrontColor);

        mPaintContent = new Paint();
        mPaintContent.setAntiAlias(true);
        mPaintContent.setDither(true);
        mPaintContent.setTextSize(mContentTextSize);
        mPaintContent.setColor(mContentColor);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mTexts != null) {
            AdEntity model = mTexts.get(mIndex);
            String font = model.getmFront();
            String back = model.getmBack();
            //绘制前缀
            Rect indexBound = new Rect();
            mPaintFront.getTextBounds(font, 0, font.length(), indexBound);

            //绘制内容文字
            Rect contentBound = new Rect();
            mPaintContent.getTextBounds(back, 0, back.length(), contentBound);
            if (mY == 0 && hasInit == false) {
                mY = getMeasuredHeight() - indexBound.top;
                hasInit = true;
            }
            //移动到最上面
            if (mY <= 0 - indexBound.bottom) {
                KLog.i(TAG, "onDraw: " + getMeasuredHeight());
                mY = getMeasuredHeight() - indexBound.top;
                mIndex++;
                isPaused = false;
            }
            canvas.drawText(back, 0, back.length(), (indexBound.right - indexBound.left) + 20, mY, mPaintContent);
            canvas.drawText(font, 0, font.length(), 10, mY, mPaintFront);
            //移动到中间
            if (!isPaused && mY <= getMeasuredHeight() / 2 - (indexBound.top + indexBound.bottom) / 2) {
                isMove = false;
                isPaused = true;
                Timer timer = new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        postInvalidate();
                        isMove = true;
                    }
                }, mInterval);
            }
            mY -= mSpeed;
            //循环使用数据
            if (mIndex == mTexts.size()) {
                mIndex = 0;
            }
            //如果是处于移动状态时的,则延迟绘制
            //计算公式为一个比例,一个时间间隔移动组件高度,则多少毫秒来移动1像素
            if (isMove) {
                postInvalidateDelayed(2);
            }
        }

    }
}

至此这个自定义View就完成了,有不足的地方欢迎指出,另外建了个新手交流Android开发的QQ群,欢迎加入.

群号:375276053

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 一、概述 1. 四线格与基线 小时候,我们在刚开始学习写字母时,用的本子是四线格的,我们必须把字母按照规则写在四线...
    addapp阅读 7,629评论 2 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,825评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,373评论 0 17
  • 前言: 在接触Android这么长时间,看到很多大牛都在和大家分享自己的知识,深有体会,刚好前段时间写了一个Dem...
    杨艳伟阅读 1,257评论 0 5
  • 好像上班以来睡得就比在学校的早些呢,早上总是在闹钟响之前就醒了,起来后洗漱收拾,有时候做点吃的就出门了。往公司来的...
    倚马看落日阅读 200评论 0 0