2022-01-03

1.SurfaceView详解

2.双缓冲技术

3.如何去使用SurfaceView

# 二、自定义view

## 什么是自定义view:

主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.自定义View 我们大部分时候只需重写两个函数: onMeasure(),onDraw(). onMeasure()负责对当前View 的尺寸进行测量,onDraw负责把当前这个View绘制出来,当然了,还需要写构造函数。

public Views(Context context) {

    super(context);

}

public Views(Context context,  AttributeSet attrs) {

    super(context, attrs);

}

# 三、如何实现自定义View?

## 三种方式:1. **组合控件** 、2 .自绘控件、3.  **继承控件**

![img](https://upload-images.jianshu.io/upload_images/10294405-49437ed26f19f7d5.png?imageMogr2/auto-orient/strip|imageView2/2/w/557/format/webp)

# 四、自定义View基础

## 4.1 分类

自定义View的实现方式有以下几种

| 类型              | 定义                                                        |

| ----------------- | ------------------------------------------------------------ |

| 自定义组合控件    | 多个控件组合成为一个新的控件,方便多处复用                  |

| 继承系统View控件  | 继承自TextView等系统控件,在系统控件的基础功能上进行扩展    |

| 继承View          | 不复用系统控件逻辑,继承View进行功能定义                    |

| 继承系统ViewGroup | 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 |

| 继承ViewViewGroup | 不复用系统控件逻辑,继承ViewGroup进行功能定义                |

## 4.2 View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

| 函数      | 作用                        | 相关方法                                    |

| --------- | ---------------------------- | -------------------------------------------- |

| measure() | 测量View的宽高              | measure(),setMeasuredDimension(),onMeasure() |

| layout()  | 计算当前View以及子View的位置 | layout(),onLayout(),setFrame()              |

| draw()    | 视图的绘制工作              | draw(),onDraw()                              |

## 4.3 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

![img](https://upload-images.jianshu.io/upload_images/10294405-a57f0f703dca0eb4.png?imageMogr2/auto-orient/strip|imageView2/2/w/491/format/webp)

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

![img](https://upload-images.jianshu.io/upload_images/10294405-4ca426e6a92db696.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)

## 4.4 View获取自身高度

由上图可算出View的高度:

- width = getRight() -  getLeft();

- height  =  getBottom()  -  getTop();

View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度和高度,其内部方法和上文所示是相同的,我们可以直接调用来获取View得宽高。

## 4.5 View自身的坐标

通过如下方法可以获取View到其父控件的距离。

```

getTop();获取View到其父布局顶边的距离。

getLeft();获取View到其父布局左边的距离。

getBottom();获取View到其父布局顶边的距离。

getRight();获取View到其父布局左边的距离。

```

## 4.6 构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建`TestView`,

```java

public class TestView extends View {

    /**

    * 在java代码里new的时候会用到

    * @param context

    */

    public TestView(Context context) {

        super(context);

    }

    /**

    * 在xml布局文件中使用时自动调用

    * @param context

    */

    public TestView(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

    }

    /**

    * 不会自动调用,如果有默认style时,在第二个构造函数中调用

    * @param context

    * @param attrs

    * @param defStyleAttr

    */

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

    }

    /**

    * 只有在API版本>21时才会用到

    * 不会自动调用,如果有默认style时,在第二个构造函数中调用

    * @param context

    * @param attrs

    * @param defStyleAttr

    * @param defStyleRes

    */

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {

        super(context, attrs, defStyleAttr, defStyleRes);

    }

}

```

## 4.7 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。

Android自定义属性可分为以下几步:

```

1.自定义一个View

2.编写values/attrs.xml,在其中编写styleable和item等标签元素

3.在布局文件中View使用自定义的属性(注意namespace)

4.在View的构造方法中通过TypedArray获取

```

## 4.8 实例说明

自定义属性的声明文件

```xml

    <?xml version="1.0" encoding="utf-8"?>

    <resources>

        <declare-styleable name="test">

            <attr name="text" format="string" />

            <attr name="testAttr" format="integer" />

        </declare-styleable>

    </resources>

```

自定义View类

```java

public class MyTextView extends View {

    private static final String TAG = MyTextView.class.getSimpleName();

    //在View的构造方法中通过TypedArray获取

    public MyTextView(Context context, AttributeSet attrs) {

        super(context, attrs);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);

        String text = ta.getString(R.styleable.test_testAttr);

        int textAttr = ta.getInteger(R.styleable.test_text, -1);

        Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);

        ta.recycle();

    }

}

```

布局文件中使用

```xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    xmlns:app="http://schemas.android.com/apk/res/com.example.test"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

    <com.example.test.MyTextView

        android:layout_width="100dp"

        android:layout_height="200dp"

        app:testAttr="520"

        app:text="helloworld" />

</RelativeLayout>

```

## 4.9 属性值的类型format

###  (1). reference:参考某一资源ID

* 属性定义

```xml

<declare-styleable name = "名称">

    <attr name = "background" format = "reference" />

</declare-styleable>

```

* 属性使用

```xml

<ImageView android:background = "@drawable/图片ID"/>

```

###  (2).  color:颜色值

* 属性定义:

```xml

<attr name = "textColor" format = "color" />

```

* 属性使用 :

```xml

<TextView android:textColor = "#00FF00" />

```

(3). boolean:布尔值

- 属性定义:

```xml

<attr name = "focusable" format = "boolean" />

```

- 属性使用:

```xml

<Button android:focusable = "true"/>

```

(4). dimension:尺寸值

- 属性定义:

```xml

<attr name = "layout_width" format = "dimension" />

```

- 属性使用:

```xml

<Button android:layout_width = "42dip"/>

```

(5). float:浮点值

- 属性定义:

```xml

<attr name = "fromAlpha" format = "float" />

```

- 属性使用:

```xml

<alpha android:fromAlpha = "1.0"/>

```

(6). integer:整型值

- 属性定义:

```xml

<attr name = "framesCount" format="integer" />

```

- 属性使用:

```xml

<animated-rotate android:framesCount = "12"/>

```

(7). string:字符串

- 属性定义:

```xml

<attr name = "text" format = "string" />

```

- 属性使用:

```xml

<TextView android:text = "我是文本"/>

```

(8). fraction:百分数

- 属性定义:

```xml

<attr name = "pivotX" format = "fraction" />

```

- 属性使用:

```xml

<rotate android:pivotX = "200%"/>

```

(9). enum:枚举值

- 属性定义:

```xml

<declare-styleable name="名称">

    <attr name="orientation">

        <enum name="horizontal" value="0" />

        <enum name="vertical" value="1" />

    </attr>

</declare-styleable>

```

- 属性使用:

```xml

<LinearLayout 

    android:orientation = "vertical">

</LinearLayout>

```

注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"

(10). flag:位或运算

- 属性定义:

```xml

<declare-styleable name="名称">

    <attr name="gravity">

            <flag name="top" value="0x01" />

            <flag name="bottom" value="0x02" />

            <flag name="left" value="0x04" />

            <flag name="right" value="0x08" />

            <flag name="center_vertical" value="0x16" />

            ...

    </attr>

</declare-styleable>

```

- 属性使用:

```xml

<TextView android:gravity="bottom|left"/>

```

注意:位运算类型的属性在使用的过程中可以使用多个值

(11). 混合类型:属性定义时可以指定多种类型值

- 属性定义:

```xml

<declare-styleable name = "名称">

    <attr name = "background" format = "reference|color" />

</declare-styleable>

```

* 属性使用:

```xml

<ImageView

android:background = "@drawable/图片ID" />

或者:

<ImageView

android:background = "#00FF00" />

```

# 五 、View绘制流程

> 这一章节偏向于解释View绘制的源码实现,可以更好地帮助我们掌握整个绘制过程。

View的绘制基本由measure()、layout()、draw()这个三个函数完成

| 函数      | 作用                        | 相关方法                                    |

| --------- | ---------------------------- | -------------------------------------------- |

| measure() | 测量View的宽高              | measure(),setMeasuredDimension(),onMeasure() |

| layout()  | 计算当前View以及子View的位置 | layout(),onLayout(),setFrame()              |

| draw()    | 视图的绘制工作              | draw(),onDraw()                              |

## 5.1 Measure()

### MeasureSpec

`MeasureSpec`是View的内部类,它封装了一个View的尺寸,在`onMeasure()`当中会根据这个`MeasureSpec`的值来确定View的宽高。

`MeasureSpec`的值保存在一个int值当中。一个int值有32位,前两位表示`模式mode`后30位表示`大小size`。即`MeasureSpec` = `mode` + `size`。

在`MeasureSpec`当中一共存在三种`mode`:`UNSPECIFIED`、`EXACTLY` 和

`AT_MOST`。

对于View来说,`MeasureSpec`的mode和Size有如下意义

| 模式        | 意义                                                        | 对应            |

| ----------- | ------------------------------------------------------------ | ---------------- |

| EXACTLY    | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent    |

| AT_MOST    | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content    |

| UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大  | 一般系统内部使用 |

> 使用方式

```cpp

    // 获取测量模式(Mode)

    int specMode = MeasureSpec.getMode(measureSpec)

    // 获取测量大小(Size)

    int specSize = MeasureSpec.getSize(measureSpec)

    // 通过Mode 和 Size 生成新的SpecMode

    int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

```

在View当中,`MeasureSpace`的测量代码如下:

```cpp

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

        int specMode = MeasureSpec.getMode(spec);

        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;

        int resultMode = 0;

        switch (specMode) {

        //当父View要求一个精确值时,为子View赋值

        case MeasureSpec.EXACTLY:

            //如果子view有自己的尺寸,则使用自己的尺寸

            if (childDimension >= 0) {

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

                //当子View是match_parent,将父View的大小赋值给子View

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                resultSize = size;

                resultMode = MeasureSpec.EXACTLY;

                //如果子View是wrap_content,设置子View的最大尺寸为父View

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            }

            break;

        // 父布局给子View了一个最大界限

        case MeasureSpec.AT_MOST:

            if (childDimension >= 0) {

                //如果子view有自己的尺寸,则使用自己的尺寸

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                // 父View的尺寸为子View的最大尺寸

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                //父View的尺寸为子View的最大尺寸

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            }

            break;

        // 父布局对子View没有做任何限制

        case MeasureSpec.UNSPECIFIED:

            if (childDimension >= 0) {

            //如果子view有自己的尺寸,则使用自己的尺寸

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0

                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

                resultMode = MeasureSpec.UNSPECIFIED;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0

                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

                resultMode = MeasureSpec.UNSPECIFIED;

            }

            break;

        }


        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

    }

```

> 这里需要注意,这段代码只是在为子View设置`MeasureSpec`参数而不是实际的设置子View的大小。子View的最终大小需要在View中具体设置。

从源码可以看出来,子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的。

在测量子View大小时:

| 父View mode | 子View                                                      |

| ----------- | ------------------------------------------------------------ |

| UNSPECIFIED | 父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0 |

| EXACTLY    | 父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围 |

| AT_MOST    | 父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围 |

### onMeasure()

整个测量过程的入口位于`View`的`measure`方法当中,该方法做了一些参数的初始化之后调用了`onMeasure`方法,这里我们主要分析`onMeasure`。

`onMeasure`方法的源码如下:

```cpp

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

    }

```

很简单这里只有一行代码,涉及到了三个方法我们挨个分析。

-  **setMeasuredDimension(int measuredWidth, int measuredHeight)** :该方法用来设置View的宽高,在我们自定义View时也会经常用到。

-  **getDefaultSize(int size, int measureSpec)**:该方法用来获取View默认的宽高,结合源码来看。

```cpp

/**

*  有两个参数size和measureSpec

*  1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。

*  2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式

*/

public static int getDefaultSize(int size, int measureSpec) {

        int result = size;

        int specMode = MeasureSpec.getMode(measureSpec);

        int specSize = MeasureSpec.getSize(measureSpec);

        //从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。

        switch (specMode) {

        case MeasureSpec.UNSPECIFIED:

            result = size;

            break;

        case MeasureSpec.AT_MOST:

        case MeasureSpec.EXACTLY:

            result = specSize;

            break;

        }

        return result;

    }

```

-  **getSuggestedMinimumWidth()**:getHeight和该方法原理是一样的,这里只分析这一个。

```csharp

//当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0.

//如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。

protected int getSuggestedMinimumWidth() {

        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

    }

```

------

`ViewGroup`的测量过程与View有一点点区别,其本身是继承自`View`,它没有对`View`的`measure`方法以及`onMeasure`方法进行重写。

为什么没有重写`onMeasure`呢?ViewGroup除了要测量自身宽高外还需要测量各个子`View`的大小,而不同的布局测量方式也都不同(可参考`LinearLayout`以及`FrameLayout`),所以没有办法统一设置。因此它提供了测量子`View`的方法`measureChildren()`以及`measureChild()`帮助我们对子View进行测量。

`measureChildren()`以及`measureChild()`的源码这里不再分析,大致流程就是遍历所有的子View,然后调用`View`的`measure()`方法,让子`View`测量自身大小。具体测量流程上面也以及介绍过了

------

`measure`过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下`LinearLayout`的`onMeasure`方法。

## 5.2 Layout()

> 要计算位置首先要对Android坐标系有所了解,前面的内容我们也有介绍过。

`layout()`过程,对于`View`来说用来计算`View`的位置参数,对于`ViewGroup`来说,除了要测量自身位置,还需要测量子`View`的位置。

`layout()`方法是整个Layout()流程的入口,看一下这部分源码

```cpp

/**

*  这里的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。

*

*/

public void layout(int l, int t, int r, int b) {

        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {

            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);

            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

        }

        int oldL = mLeft;

        int oldT = mTop;

        int oldB = mBottom;

        int oldR = mRight;

        //这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。

        boolean changed = isLayoutModeOptical(mParent) ?

                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

            onLayout(changed, l, t, r, b);

        }

    }

```

从源码我们知道,在`layout()`方法中已经通过`setOpticalFrame(l, t, r, b)`或 `setFrame(l, t, r, b)`方法对View自身的位置进行了设置,所以`onLayout(changed, l, t, r, b)`方法主要是`ViewGroup`对子View的位置进行计算。

> 有兴趣的可以看一下`LinearLayout`的`onLayout`源码,可以帮助加深理解。

## 5.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在`View`的`draw()`方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

1. 如果需要,绘制背景。

2. 有过有必要,保存当前canvas。

3. 绘制View的内容。

4. 绘制子View。

5. 如果有必要,绘制边缘、阴影等效果。

6. 绘制装饰,如滚动条等等。

通过各个步骤的源码再做分析:

```java

    public void draw(Canvas canvas) {


        int saveCount;

        // 1. 如果需要,绘制背景

        if (!dirtyOpaque) {

            drawBackground(canvas);

        }

        // 2. 有过有必要,保存当前canvas。

        final int viewFlags = mViewFlags;


        if (!verticalEdges && !horizontalEdges) {

            // 3. 绘制View的内容。

            if (!dirtyOpaque) onDraw(canvas);

            // 4. 绘制子View。

            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground

            if (mOverlay != null && !mOverlay.isEmpty()) {

                mOverlay.getOverlayView().dispatchDraw(canvas);

            }

            // 6. 绘制装饰,如滚动条等等。

            onDrawForeground(canvas);

            // we're done...

            return;

        }

    }


    /**

    *  1.绘制View背景

    */

    private void drawBackground(Canvas canvas) {

        //获取背景

        final Drawable background = mBackground;

        if (background == null) {

            return;

        }

        setBackgroundBounds();

        //获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。

        final int scrollX = mScrollX;

        final int scrollY = mScrollY;

        if ((scrollX | scrollY) == 0) {

            background.draw(canvas);

        } else {

            canvas.translate(scrollX, scrollY);

            background.draw(canvas);

            canvas.translate(-scrollX, -scrollY);

        }

    }


    /**

    * 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。

    */

    protected void onDraw(Canvas canvas) {

    }


    /**

    * 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。

    *  在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。

    */

    protected void dispatchDraw(Canvas canvas) {

    }


```

## 5.3 自定义组合控件

自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以把他们组合成一个新的控件。

我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。

**1. 编写布局文件**

```xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:id="@+id/header_root_layout"

    android:layout_height="45dp"

    android:background="#827192">

    <ImageView

        android:id="@+id/header_left_img"

        android:layout_width="45dp"

        android:layout_height="45dp"

        android:layout_alignParentLeft="true"

        android:paddingLeft="12dp"

        android:paddingRight="12dp"

        android:src="@drawable/back"

        android:scaleType="fitCenter"/>

    <TextView

        android:id="@+id/header_center_text"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_centerInParent="true"

        android:lines="1"

        android:maxLines="11"

        android:ellipsize="end"

        android:text="title"

        android:textStyle="bold"

        android:textColor="#ffffff"/>


    <ImageView

        android:id="@+id/header_right_img"

        android:layout_width="45dp"

        android:layout_height="45dp"

        android:layout_alignParentRight="true"

        android:src="@drawable/add"

        android:scaleType="fitCenter"

        android:paddingRight="12dp"

        android:paddingLeft="12dp"/>

</RelativeLayout>

```

布局很简单,中间是title的文字,左边是返回按钮,右边是一个添加按钮。

**2. 实现构造方法**

```java

//因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。

//关于各个构造方法的介绍可以参考前面的内容

public class YFHeaderView extends RelativeLayout {

    public YFHeaderView(Context context) {

        super(context);

    }

    public YFHeaderView(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

    }

}

```

**3. 初始化UI**

```csharp

    //初始化UI,可根据业务需求设置默认值。

    private void initView(Context context) {

        LayoutInflater.from(context).inflate(R.layout.view_header, this, true);

        img_left = (ImageView) findViewById(R.id.header_left_img);

        img_right = (ImageView) findViewById(R.id.header_right_img);

        text_center = (TextView) findViewById(R.id.header_center_text);

        layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);

        layout_root.setBackgroundColor(Color.BLACK);

        text_center.setTextColor(Color.WHITE);

    }


```

**4. 提供对外的方法**

可以根据业务需求对外暴露一些方法。

```cpp

    //设置标题文字的方法

    private void setTitle(String title) {

        if (!TextUtils.isEmpty(title)) {

            text_center.setText(title);

        }

    }

    //对左边按钮设置事件的方法

    private void setLeftListener(OnClickListener onClickListener) {

        img_left.setOnClickListener(onClickListener);

    }

    //对右边按钮设置事件的方法

    private void setRightListener(OnClickListener onClickListener) {

        img_right.setOnClickListener(onClickListener);

    }

```

**5. 在布局当中引用该控件**

```xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:orientation="vertical"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

    <com.example.yf.view.YFHeaderView

        android:layout_width="match_parent"

        android:layout_height="45dp">

    </com.example.yf.view.YFHeaderView>

</LinearLayout>

```

到这里基本的功能已经有了。除了这些基础功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时候就需要用到自定义属性来解决了。

前面已经简单介绍过自定义属性的相关知识,我们之间看代码

**1.首先在values目录下创建attrs.xml**

内容如下:

```xml

<resources>

    <declare-styleable name="HeaderBar">

        <attr name="title_text_clolor" format="color"></attr>

        <attr name="title_text" format="string"></attr>

        <attr name="show_views">

            <flag name="left_text" value="0x01" />

            <flag name="left_img" value="0x02" />

            <flag name="right_text" value="0x04" />

            <flag name="right_img" value="0x08" />

            <flag name="center_text" value="0x10" />

            <flag name="center_img" value="0x20" />

        </attr>

    </declare-styleable>

</resources>

```

这里我们定义了三个属性,文字内容、颜色以及要显示的元素。

**2.在java代码中进行设置**

```csharp

    private void initAttrs(Context context, AttributeSet attrs) {

        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);

        //获取title_text属性

        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);

        if (!TextUtils.isEmpty(title)) {

            text_center.setText(title);

        }

        //获取show_views属性,如果没有设置时默认为0x26

        showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);

        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));

        mTypedArray.recycle();

        showView(showView);

    }


    private void showView(int showView) {

        //将showView转换为二进制数,根据不同位置上的值设置对应View的显示或者隐藏。

        Long data = Long.valueOf(Integer.toBinaryString(showView));

        element = String.format("%06d", data);

        for (int i = 0; i < element.length(); i++) {

            if(i == 0) ;

            if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 3) ;

            if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 5) ;

        }

    }

```

**3.在布局文件中进行设置**

```xml

    <com.example.yf.view.YFHeaderView

        android:layout_width="match_parent"

        android:layout_height="45dp"

        app:title_text="标题"

        app:show_views="center_text|left_img|right_img">

    </com.example.yf.view.YFHeaderView>

```

OK,到这里整个View基本定义完成。整个YFHeaderView的代码如下

```java

public class YFHeaderView extends RelativeLayout {

    private ImageView img_left;

    private TextView text_center;

    private ImageView img_right;

    private RelativeLayout layout_root;

    private Context context;

    String element;

    private int showView;

    public YFHeaderView(Context context) {

        super(context);

        this.context = context;

        initView(context);

    }

    public YFHeaderView(Context context, AttributeSet attrs) {

        super(context, attrs);

        this.context = context;

        initView(context);

        initAttrs(context, attrs);

    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        this.context = context;

        initView(context);

        initAttrs(context, attrs);

    }

    private void initAttrs(Context context, AttributeSet attrs) {

        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);

        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);

        if (!TextUtils.isEmpty(title)) {

            text_center.setText(title);

        }

        showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);

        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));

        mTypedArray.recycle();

        showView(showView);

    }

    private void showView(int showView) {

        Long data = Long.valueOf(Integer.toBinaryString(showView));

        element = String.format("%06d", data);

        for (int i = 0; i < element.length(); i++) {

            if(i == 0) ;

            if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 3) ;

            if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

            if(i == 5) ;

        }

    }

    private void initView(final Context context) {

        LayoutInflater.from(context).inflate(R.layout.view_header, this, true);

        img_left = (ImageView) findViewById(R.id.header_left_img);

        img_right = (ImageView) findViewById(R.id.header_right_img);

        text_center = (TextView) findViewById(R.id.header_center_text);

        layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);

        layout_root.setBackgroundColor(Color.BLACK);

        text_center.setTextColor(Color.WHITE);

        img_left.setOnClickListener(new OnClickListener() {

            @Override

            public void onClick(View view) {

                Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show();

            }

        });

    }

    private void setTitle(String title) {

        if (!TextUtils.isEmpty(title)) {

            text_center.setText(title);

        }

    }

    private void setLeftListener(OnClickListener onClickListener) {

        img_left.setOnClickListener(onClickListener);

    }

    private void setRightListener(OnClickListener onClickListener) {

        img_right.setOnClickListener(onClickListener);

    }

}

```

## 5.4  继承系统控件

继承系统的控件可以分为继承View子类(如TextVIew等)和继承ViewGroup子类(如LinearLayout等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

业务需求:为文字设置背景,并在布局中间添加一条横线。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的`onMeaseur`和`onLayout`流程,所以我们只需要重写`onDraw`方法 。实现非常简单,话不多说,直接上代码。

```java

public class LineTextView extends TextView {

    //定义画笔,用来绘制中心曲线

    private Paint mPaint;


    /**

    * 创建构造方法

    * @param context

    */

    public LineTextView(Context context) {

        super(context);

        init();

    }

    public LineTextView(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public LineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        mPaint = new Paint();

        mPaint.setColor(Color.BLACK);

    }

    //重写draw方法,绘制我们需要的中间线以及背景

    @Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        int width = getWidth();

        int height = getHeight();

        mPaint.setColor(Color.BLUE);

        //绘制方形背景

        RectF rectF = new RectF(0,0,width,height);

        canvas.drawRect(rectF,mPaint);

        mPaint.setColor(Color.BLACK);

        //绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)

        canvas.drawLine(0,height/2,width,height/2,mPaint);

    }

}

```

> 对于View的绘制还需要对`Paint()`、`canvas`以及`Path`的使用有所了解,不清楚的可以稍微了解一下。

这里的实现比较简单,因为具体实现会与业务环境密切相关,这里只是做一个参考。

## 5.5  直接继承View

直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写`onDraw`外还需要对`onMeasure`方法进行重写。

我们用自定义View来绘制一个正方形。

- **首先定义构造方法,以及做一些初始化操作**

```java

ublic class RectView extends View{

    //定义画笔

    private Paint mPaint = new Paint();

    /**

    * 实现构造方法

    * @param context

    */

    public RectView(Context context) {

        super(context);

        init();

    }

    public RectView(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        mPaint.setColor(Color.BLUE);

    }

}

```

- **重写draw方法,绘制正方形,注意对padding属性进行设置**

```java

/**

    * 重写draw方法

    * @param canvas

    */

    @Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        //获取各个编剧的padding值

        int paddingLeft = getPaddingLeft();

        int paddingRight = getPaddingRight();

        int paddingTop = getPaddingTop();

        int paddingBottom = getPaddingBottom();

        //获取绘制的View的宽度

        int width = getWidth()-paddingLeft-paddingRight;

        //获取绘制的View的高度

        int height = getHeight()-paddingTop-paddingBottom;

        //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)

        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);

    }

```

之前我们讲到过View的`measure`过程,再看一下源码对这一步的处理

```cpp

    public static int getDefaultSize(int size, int measureSpec) {

        int result = size;

        int specMode = MeasureSpec.getMode(measureSpec);

        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {

        case MeasureSpec.UNSPECIFIED:

            result = size;

            break;

        case MeasureSpec.AT_MOST:

        case MeasureSpec.EXACTLY:

            result = specSize;

            break;

        }

        return result;

    }

```

在View的源码当中并没有对`AT_MOST`和`EXACTLY`两个模式做出区分,也就是说View在`wrap_content`和`match_parent`两个模式下是完全相同的,都会是`match_parent`,显然这与我们平时用的View不同,所以我们要重写`onMeasure`方法。

- **重写`onMeasure`方法**

```java

    /**

    * 重写onMeasure方法

    *

    * @param widthMeasureSpec

    * @param heightMeasureSpec

    */

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //处理wrap_contentde情况

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(300, 300);

        } else if (widthMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(300, heightSize);

        } else if (heightMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(widthSize, 300);

        }

    }

```

整个自定义View的代码如下:

```java

public class RectView extends View {

    //定义画笔

    private Paint mPaint = new Paint();

    /**

    * 实现构造方法

    *

    * @param context

    */

    public RectView(Context context) {

        super(context);

        init();

    }

    public RectView(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        mPaint.setColor(Color.BLUE);

    }

    /**

    * 重写onMeasure方法

    *

    * @param widthMeasureSpec

    * @param heightMeasureSpec

    */

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(300, 300);

        } else if (widthMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(300, heightSize);

        } else if (heightMode == MeasureSpec.AT_MOST) {

            setMeasuredDimension(widthSize, 300);

        }

    }

    /**

    * 重写draw方法

    *

    * @param canvas

    */

    @Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        //获取各个编剧的padding值

        int paddingLeft = getPaddingLeft();

        int paddingRight = getPaddingRight();

        int paddingTop = getPaddingTop();

        int paddingBottom = getPaddingBottom();

        //获取绘制的View的宽度

        int width = getWidth() - paddingLeft - paddingRight;

        //获取绘制的View的高度

        int height = getHeight() - paddingTop - paddingBottom;

        //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)

        canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);

    }

}

```

整个过程大致如下,直接继承View时需要有几点注意:

> 1、在onDraw当中对padding属性进行处理。

>  2、在onMeasure过程中对wrap_content属性进行处理。

>  3、至少要有一个构造方法。

## 5.6 继承ViewGroup

自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。

## 5.7 需求实例

实现一个类似于Viewpager的可左右滑动的布局。

代码比较多,我们结合注释分析。

```java

public class HorizontaiView extends ViewGroup {

    private int lastX;

    private int lastY;

    private int currentIndex = 0;

    private int childWidth = 0;

    private Scroller scroller;

    private VelocityTracker tracker;


    /**

    * 1.创建View类,实现构造函数

    * 实现构造方法

    * @param context

    */

    public HorizontaiView(Context context) {

        super(context);

        init(context);

    }

    public HorizontaiView(Context context, AttributeSet attrs) {

        super(context, attrs);

        init(context);

    }

    public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init(context);

    }

    private void init(Context context) {

        scroller = new Scroller(context);

        tracker = VelocityTracker.obtain();

    }

    /**

    * 2、根据自定义View的绘制流程,重写`onMeasure`方法,注意对wrap_content的处理

    * 重写onMeasure方法

    * @param widthMeasureSpec

    * @param heightMeasureSpec

    */

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //获取宽高的测量模式以及测量值

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //测量所有子View

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //如果没有子View,则View大小为0,0

        if (getChildCount() == 0) {

            setMeasuredDimension(0, 0);

        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

            View childOne = getChildAt(0);

            int childWidth = childOne.getMeasuredWidth();

            int childHeight = childOne.getMeasuredHeight();

            //View的宽度=单个子View宽度*子View个数,View的高度=子View高度

            setMeasuredDimension(getChildCount() * childWidth, childHeight);

        } else if (widthMode == MeasureSpec.AT_MOST) {

            View childOne = getChildAt(0);

            int childWidth = childOne.getMeasuredWidth();

            //View的宽度=单个子View宽度*子View个数,View的高度=xml当中设置的高度

            setMeasuredDimension(getChildCount() * childWidth, heightSize);

        } else if (heightMode == MeasureSpec.AT_MOST) {

            View childOne = getChildAt(0);

            int childHeight = childOne.getMeasuredHeight();

            //View的宽度=xml当中设置的宽度,View的高度=子View高度

            setMeasuredDimension(widthSize, childHeight);

        }

    }

    /**

    * 3、接下来重写`onLayout`方法,对各个子View设置位置。

    * 设置子View位置

    * @param changed

    * @param l

    * @param t

    * @param r

    * @param b

    */

    @Override

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();

        int left = 0;

        View child;

        for (int i = 0; i < childCount; i++) {

            child = getChildAt(i);

            if (child.getVisibility() != View.GONE) {

                childWidth = child.getMeasuredWidth();

                child.layout(left, 0, left + childWidth, child.getMeasuredHeight());

                left += childWidth;

            }

        }

    }

}

```

到这里我们的View布局就已经基本结束了。但是要实现`Viewpager`的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义View的时候也是会经常用到的,不了解的可以参考这篇文章[Android Touch事件分发超详细解析](//www.greatytc.com/p/bc4c9e5f4b1c)。

```csharp

    /**

    * 4、因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。

    * 重写onInterceptTouchEvent,对横向滑动事件进行拦截

    * @param event

    * @return

    */

    @Override

    public boolean onInterceptTouchEvent(MotionEvent event) {

        boolean intercrpt = false;

        //记录当前点击的坐标

        int x = (int) event.getX();

        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_MOVE:

                int deltaX = x - lastX;

                int delatY = y - lastY;

                //当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截

                if (Math.abs(deltaX) > Math.abs(delatY)) {

                    intercrpt = true;

                }

                break;

        }

        lastX = x;

        lastY = y;

        //intercrpt = true表示对事件进行拦截

        return intercrpt;

    }


    /**

    * 5、当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。

    * 重写onTouchEvent方法

    * @param event

    * @return

    */

    @Override

    public boolean onTouchEvent(MotionEvent event) {

        tracker.addMovement(event);

        //获取事件坐标(x,y)

        int x = (int) event.getX();

        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_MOVE:

                int deltaX = x - lastX;

                int delatY = y - lastY;

                //scrollBy方法将对我们当前View的位置进行偏移

                scrollBy(-deltaX, 0);

                break;

            //当产生ACTION_UP事件时,也就是我们抬起手指

            case MotionEvent.ACTION_UP:

                //getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标

                //distance存储的就是此次滑动的距离

                int distance = getScrollX() - childWidth * currentIndex;

                //当本次滑动距离>View宽度的1/2时,切换View

                if (Math.abs(distance) > childWidth / 2) {

                    if (distance > 0) {

                        currentIndex++;

                    } else {

                        currentIndex--;

                    }

                } else {

                    //获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点

                    tracker.computeCurrentVelocity(1000);

                    float xV = tracker.getXVelocity();

                    //当X轴加速度>50时,也就是产生了快速滑动,也会切换View

                    if (Math.abs(xV) > 50) {

                        if (xV < 0) {

                            currentIndex++;

                        } else {

                            currentIndex--;

                        }

                    }

                }

                //对currentIndex做出限制其范围为【0,getChildCount() - 1】

                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;

                //滑动到下一个View

                smoothScrollTo(currentIndex * childWidth, 0);

                tracker.clear();

                break;

        }

        lastX = x;

        lastY = y;

        return true;

    }

    private void smoothScrollTo(int destX, int destY) {

        //startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离

        scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);

        //invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法

        invalidate();

    }

    //重写computeScroll方法

    @Override

    public void computeScroll() {

        super.computeScroll();

        //当scroller.computeScrollOffset()=true时表示滑动没有结束

        if (scroller.computeScrollOffset()) {

            //调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置

            scrollTo(scroller.getCurrX(), scroller.getCurrY());

            //没有滑动结束,继续刷新View

            postInvalidate();

        }

    }

```

这部分代码比较多,为了方便阅读,在代码当中进行了注释。

之后就是在XML代码当中引入自定义View

```xml

<com.example.yf.view.HorizontaiView

        android:id="@+id/test_layout"

        android:layout_width="match_parent"

        android:layout_height="400dp">

        <ListView

            android:id="@+id/list1"

            android:layout_width="match_parent"

            android:layout_height="match_parent">

        </ListView>

        <ListView

            android:id="@+id/list2"

            android:layout_width="match_parent"

            android:layout_height="match_parent">

        </ListView>

        <ListView

            android:id="@+id/list3"

            android:layout_width="match_parent"

            android:layout_height="match_parent">

        </ListView>

    </com.example.yf.view.HorizontaiView>

```

好了,可以运行看一下效果了。

# 总结

本章节详细讲解了自定义view的方式和方法,对于安卓底层的开发控件方式有一个更加深入的了解,大家需要详细的去总结练习如何去自定义view,如何去调用自定义view等方法。

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