自定义 View 之 Scroller 与 View 滑动相关完全总结

前言: 一个人学到的最重要的东西是学习的方法。——尼尔·波兹曼《娱乐至死》

在讲 Scroller 之前,咱们需要先了解一下 View 的滑动问题,看一下常见的几种让 View 能随着手指滑动起来的几种方式。

1、常见的几种可滑动 View 的方式

有如下几种:

  • 通过 View 的 ScrollBy 和 ScrollTo 方法实现滑动。
  • 通过动画给 View 施加位移效果来实现滑动。
  • 通过改变 View 的 LayoutParams 使 View 重新布局从而实现滑动。

接下来对这三种方式一一进行讲解。

1、通过 View 的 ScrollBy 和 ScrollTo 方法实现滑动

这两个方法都是用于对 View 进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy() 方法是让 View 相对于当前的位置滚动某段距离,而 scrollTo() 方法则是让 View 相对于初始的位置滚动某段距离。我们通过查看 scrollBy() 方法的源码可以发现,其方法内部其实同样调用的 scrolllTo() 方法:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

通过查看源码我们知道,scrollBy() 方法内部其实就是调用了 scrollTo() 方法,只不过参数中多加了 mScrollX 和 mScrollY 这两个变量,那么这2个变量究竟是什么呢?

mScrollX 和 mScrollY 我们可以理解为 View 的偏移量,初始值都为0。当 View 发生移动时,比如说,View 往左横向移动了100px,那么这时,mScrollX 的值则为100。相反,如果是往右移动100px,那么此时值为-100。mScrollY 则是如果往上垂直移动100px,mScrollY的值则为100,否则为-100。具体如下图所示:

image

这样讲大家理解起来可能有点费劲,我们来通过例子实验一下就知道了。

  • 布局中定义好 Layout,并且包裹好两个 button
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_scrollto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ScrollTo"/>

    <Button
        android:id="@+id/btn_scrollby"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ScrollBy"/>

</LinearLayout>

外层我们使用了一个 LinearLayout,然后在里面包含了两个按钮,一个用于触发 scrollTo 逻辑,一个用于触发 scrollBy 逻辑。

  • 修改 MainActivity 代码:
public class MainActivity extends AppCompatActivity {

    private LinearLayout mLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLayout = (LinearLayout) findViewById(R.id.layout);
        findViewById(R.id.btn_scrollto).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("test", "点击前getScrollX值:" + mLayout.getScrollX());
                Log.e("test", "点击前getScrollY值:" + mLayout.getScrollY());
                mLayout.scrollTo(-50, -50);
                Log.e("test", "点击后getScrollX值:" + mLayout.getScrollX());
                Log.e("test", "点击后getScrollY值:" + mLayout.getScrollY());
            }
        });

        findViewById(R.id.btn_scrollby).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mLayout.scrollBy(-50, -50);
            }
        });
    }
}

没错,代码就是这么简单。当点击了 scrollTo 按钮时,我们调用了 LinearLayout 的 scrollTo() 方法,当点击了 scrollBy 按钮时,调用了 LinearLayout 的 scrollBy() 方法。
这里一定要注意,不管是 scrollTo() 还是 scrollBy() 方法,滚动的都是该 View 内部的内容,而LinearLayout 中的内容就是我们的两个 Button,如果你直接调用 button 的 scroll 方法的话,那结果一定不是你想看到的。
另外还有一点需要注意,就是两个 scroll 方法中传入的参数,第一个参数 x 表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数 y 表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。
运行结果:

image

运行的log:

image

通过上面的实践可以得知:
当我们执行 scrollTo() 方法,Layout 中的 View,只会移动到 scrollTo() 方法参数指定的位置上。而 scrollBy() 方法则是加上了 scrollX 和 scrollY 的值,所以每点击一次,都会动态根据当前位置移动。
现在我们再来回头看一下这两个方法的区别,scrollTo() 方法是让 View 相对于初始的位置滚动某段距离,由于 View 的初始位置是不变的,因此不管我们点击多少次 scrollTo 按钮滚动到的都将是同一个位置。而 scrollBy() 方法则是让 View 相对于当前的位置滚动某段距离,那每当我们点击一次 scrollBy 按钮,View 的当前位置都进行了变动,因此不停点击会一直向右下方移动。

2、通过动画给 View 施加位移效果来实现滑动

这种方式后面咱们会详解(在自定义 View 动画篇章讲解),这里先简单介绍一下:
通过动画来实现 View 的滑动,主要是操作 View 的 translationX 和 translationY 这两个属性。我们既可以使用补间动画也可以使用属性动画。下面分别实现下用补间和属性两种动画实现将 Button 往右移动 100px:(补间动画已经很少使用了)

  • 补间动画
TranslateAnimation anim = new TranslateAnimation(0, 100, 0, 0);
anim.setDuration(300);
anim.start();
btn.setAnimation(anim);

  • 属性动画
ObjectAnimator anim = ObjectAnimator.ofFloat(btn, "translationX", 0, 100);
anim.setDuration(300);
anim.start();

使用动画移动 View 时,需要注意的是 View 动画是对 View 的影像做操作的。移动后其实并不能真正改变 View 的位置参数,包括宽/高。并且使用补间动画时,View 并不能响应移动后位置的事件,属性动画可以响应移动后的位置事件。

3、通过改变 View 的 LayoutParams 使 View 重新布局从而实现滑动

这种方式其实我们在案例以及文章中已经无意间讲完了,因为很多自定义 View 都使用 LayoutParams 来改变位置或者其他的参数属性。这里也只是简单介绍:

通过改变 View 的布局的参数实现 View 的滑动,其用到的类是:LayoutParams。这个类相当于一个 Layout 的信息包,里面封装了关于 Layout 的宽、高等信息。如果我们想将一个 View 往右移动,那么就可以不断的增加 View 的 leftMargin:

ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
lp.leftMargin += 100;
view.setLayoutParams(lp);

通过这种方式实现的 View 的移动,View 移动后的位置就是其真实的位置。
看一个案例来讲解一个什么叫 View 移动后的位置就是其真实的位置:

下面这个实例,自定义了一个 View 继承自 ImageView,该 ImageView 可以跟随手指的触摸移动。同时在布局中还定义了一个 Button,点击该 Button 修改 ImageView 的背景。
View:

public class MoveView extends ImageView{

    public MoveView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    int lastX = 0;
    int lastY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                  layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX , getBottom() + offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
}

注意这里使用 layout() 来改变自定义 ImageView 的位置。

布局:

<com.airsaid.viewdemo.widget.MoveView
    android:id="@+id/moveView"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:background="@mipmap/ic_launcher"/>

<Button
    android:onClick="set"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:text="设置ImageView背景"/>

点击事件:

public void set(View v) {
    mMoveView.setBackgroundColor(Color.RED);
}

运行结果:

image

通过运行结果我们可以看到,该 ImageView 可以跟随手指的移动,但是当我们重新设置 ImageView 的背景后,该 ImageView 立马又回到原来位置了?

出现这个情况,其实就是因为 View 虽然移动了,但是其位置属性依然还在原来的位置,这时候如果想要在更改 ImageView 背景后,View 依然在原地,那么就可以使用上面第三种方法实现 View 的移动了。

我们将 ACTION_MOVE 中移动 View 的代码修改为:

case MotionEvent.ACTION_MOVE:
    int offsetX = x - lastX;
    int offsetY = y - lastY;
 /*  
    layout(getLeft() + offsetX, getTop() + offsetY,
            getRight() + offsetX , getBottom() + offsetY);*/

    ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
    lp.leftMargin = getLeft() + offsetX;
    lp.topMargin = getTop() + offsetY;
    setLayoutParams(lp);
    break;

运行结果:

image

总结
上面介绍了三种常见的 View 移动方式,这三种都可以实现 View 的移动。其中:

  • scrollTo()/scrollBy():操作简单,但是只能够对View内容进行移动。
  • 动画:使用简单,主要用于没有交互的View和实现复杂的动画效果。
  • 改变布局参数:操作稍复杂,适用于有交互的View。

在具体的使用中,可以根据具体使用情况选择适合的方式进行View的移动。


2、Scroller完全解析

Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤:

  • 第一步:创建 Scroller 对象。
Scroller mScroller = new Scroller(context);

  • 第二步:调用 startScroll() 方法。
mScroller.startScroll(getScrollX(), getScrollY() , -100, -100);
invalidate();

  • 第三步:重写 View 的 computeScroll() 方法,实现具体滑动逻辑。
@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();
    }
}

下面以一个简单的例子演示下 Scroller 这个类的用法:
首先自定义一个 LinearLayout,并在构造中创建了 Scroller 对象,提供了一个 startScroll()
方法,并且重写了 computeScroll() 方法:

public class ScrollerLinearLayout extends LinearLayout{

    private final Scroller mScroller;

    public ScrollerLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

   //触发mScroller的startScroll。
    public void startScroll(){
        mScroller.startScroll(getScrollX(), getScrollY() , -50, -50);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

在布局中用该自定义 LinearLayout 包裹了一个 Button:

<?xml version="1.0" encoding="utf-8"?>
<com.airsaid.viewdemo.widget.ScrollerLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.airsaid.viewdemo.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="start"
        android:text="start"/>

</com.airsaid.viewdemo.widget.ScrollerLinearLayout>

在代码中调用了其 startScroll() 方法:

public class MainActivity extends AppCompatActivity {

    private ScrollerLinearLayout mLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLayout = (ScrollerLinearLayout) findViewById(R.id.layout);
    }

    public void start(View v){
        mLayout.startScroll();
    }
}

运行结果:

image

通过上面的例子,简单实现了用 Scroller 实现了 View 的弹性滑动。但是这一切都是怎么一个过程呢?首先来看看Scroller的startScroll()方法:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

通过查看 startScroll() 方法的源码可以发现,其内部只是做了赋值的操作,并没有调用其他的方法,那么这个弹性滑动的效果又是从哪里来的呢?
答案就在于,我们在调用 startScroll() 方法后又调用了 invalidate() 方法使 View 进行了重绘。那么这时就会走 View 的 draw 方法,在 View 的 draw 方法中又会去调用 View 的 computeScroll() 方法,而该方法在 View 中是一个空实现。这就解释了我们为什么需要复写 computeScroll() 方法。
在 computeScroll() 方法中,我们调用了 computeScrollOffset() 方法进行判断,该方法内部会根据时间的流逝来计算出 scrollX 和 scrollY 改变的百分比并计算出当前的值。这个方法的返回值也很重要,返回 true 表示滑动还未结束,false 表示滑动已经结束。所以在这里,我们进行了判断,当其返回 true 时,就调用 scrollTo() 方法使 View 滑动,并调用 invalidate() 重绘,只要滑动没有完成就继续递归下去。

到这里 Scroller 的工作原理就很清晰了,Scroller 并不能单独完成 View 的弹性滑动,而是要配合 View 的 computeScroll() 方法,不断的通过 invalidate() 方法重绘 View,计算 Scroller 的 X 和 Y 值, 并通过 scrollTo() 方法一点点的移动,连在一起,就形成了弹性滑动。

接下来这个案例出自郭霖的 Scroller 案例 自定义简易的 ViewPager ,可以作为 Scroller 进阶知识,而且并不难理解。

新建一个 ScrollerLayout 并让它继承自 ViewGroup 来作为我们的简易 ViewPager 布局,代码如下所示:

public class ScrollerLayout extends ViewGroup {

    /**
     * 用于完成滚动操作的实例
     */
    private Scroller mScroller;

    /**
     * 判定为拖动的最小移动像素数
     */
    private int mTouchSlop;

    /**
     * 手机按下时的屏幕坐标
     */
    private float mXDown;

    /**
     * 手机当时所处的屏幕坐标
     */
    private float mXMove;

    /**
     * 上次触发ACTION_MOVE事件时的屏幕坐标
     */
    private float mXLastMove;

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 第一步,创建Scroller的实例
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        // 获取TouchSlop值
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为ScrollerLayout中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 为ScrollerLayout中的每一个子控件在水平方向上进行布局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
            // 初始化左右边界值
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount() - 1).getRight();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

首先在 ScrollerLayout 的构造函数里面我们进行了上述步骤中的第一步操作,即创建 Scroller 的实例,由于 Scroller 的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的 TouchSlop 的值,这个值在后面将用于判断当前用户的操作是否是拖动。
接着重写 onMeasure() 方法和 onLayout() 方法,在 onMeasure() 方法中测量 ScrollerLayout 里的每一个子控件的大小,在 onLayout() 方法中为 ScrollerLayout 里的每一个子控件在水平方向上进行布局。
接着重写 onInterceptTouchEvent() 方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于 TouchSlop 值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。
那么当我们把事件拦截掉之后,就会将事件交给 ScrollerLayout 的 onTouchEvent() 方法来处理。如果当前事件是 ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的 scrollBy() 方法,用户拖动了多少这里就 scrollBy 多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用 scrollTo() 方法来回到边界位置。
如果当前事件是 ACTION_UP 时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助 Scroller 来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用 startScroll() 方法来初始化滚动数据并刷新界面。startScroll() 方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。
现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写 computeScroll() 方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll() 方法是会一直被调用的,因此我们需要不断调用 Scroller 的 computeScrollOffset() 方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用 scrollTo() 方法,并把 Scroller 的 curX 和 curY 坐标传入,然后刷新界面从而完成平滑滚动的操作。
现在 ScrollerLayout 已经准备好了,接下来我们修改 activity_main.xml 布局中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is first child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is second child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is third child view"/>

</com.example.guolin.scrollertest.ScrollerLayout>

可以看到,这里我们在 ScrollerLayout 中放置了三个按钮用来进行测试,其实这里不仅可以放置按钮,放置任何控件都是没问题的。
最后 MainActivity 当中删除掉之前测试的代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

运行一下程序来看一看效果:

image

其实借助 Scroller,很多漂亮的滚动效果都可以轻松完成,比如实现图片轮播之类的特效。当然就目前这一个例子来讲,我们只是借助它来学习了一下 Scroller 的基本用法,例子本身有很多的功能点都没有去实现,比如说 ViewPager 会根据用户手指滑动速度的快慢来决定是否要翻页,这个功能在我们的例子中并没有体现出来。


3、其它弹性滑动效果

  • 动画
    动画上面已经介绍过了,对于更详细的知识,在《自定义 View 动画篇》进行详解。
  • 延时策略
    这种方式也不常见,这里作为了解内容。
    延时策略就是通过发送一系列的延时消息从而达到一种渐进式的效果。我们可以使用 Handler 的 postDelayed 方法,也可以使用其他的延迟消息方法。这里我们就用 Handler 来实现一个简单的弹性滑动效果:
    PS:在自定义 View 里面能不使用 Handler 就尽量不使用。
rivate int mStartX = 0;
Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case 0:
                if(mStartX > -100){
                    mLayout.scrollTo(mStartX, 0);
                    mStartX --;
                    mHandler.sendEmptyMessageDelayed(0, 5);
                }
                break;
        }
    }
};

点击按钮时,发送延时消息:

mHandler.sendEmptyMessageDelayed(0, 5);

运行结果:

image

总结
通过了解以上几种实现弹性滑动的方法,知道了可以用 Scroller 实现 View 内的弹性滑动,使用动画可以实现 View 的弹性滑动。 在这几种方式中,其实更重要的是实现思想。只要掌握了其思想,那么就可以灵活运用起来,实现更多复杂的效果。

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

推荐阅读更多精彩内容

  • 一滑动效果的产生 滑动一个View,本质区别就是移动一个View。改变当前View所在的坐标,原理和动画相似不断改...
    猿万阅读 9,671评论 0 13
  • 主要总结了: View的基础知识:View的mTop、mLeft、mRight、mBottom四个参数和对应的四个...
    Utte阅读 1,640评论 0 0
  • 什么是View View 是 Android 中所有控件的基类。 View的位置参数 View 的位置由它的四个顶...
    acc8226阅读 1,084评论 0 7
  • “卯日星君今日哪根筋搭错了!这日头布的真是毒辣,晒坏了你我还没得所谓,这要晒着了父皇母后,可还了得。云歌,你说是也...
    君子琴阅读 254评论 0 0
  • 上天让谁灭亡,总是先让他膨胀。 时间是筛子,最终会淘去一切沉渣。 蜜蜂盗花,结果却使花开茂盛。 暗透了,更能看得见星光。
    叶落在寒冷的冬季阅读 317评论 0 0