Banner

Banner

基本上所有的App首页都包含一个轮播器,一般称之为Banner。通过这一个组件可以实现以下几个功能:

  • 图片循环播放。
  • 可以添加标题文字。
  • 播放动画可以是自动的也可以是用户手动触发的。

基本原理

轮播器组件循环播放主要是横向的,所有第一时间想到了ViewPager来实现,并且利用自定义View的创建符合控件的方法。

这个思路有个小问题,当ViewPager滚动到最后一个item时,不会自动重新回到第一个item,或者回到第一个item时效果非常差。经过Google后发现两种解决思路:

  1. 通过给PagerAdapter.getCount()返回一个很大的数字来实现循环播放。
  2. 通过ViewPager.setCurrentItem(pos, false)来取消最后一个到第一个的动画。

实现

实现上述两种方案之前,先做一些准备工作,编写一下复用布局文件:

Banner布局

<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/id_viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <!-- 指示器容器 -->
    <LinearLayout
        android:id="@+id/id_dots_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="8dp"/>
</merge>

Banner ViewPager Item布局

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/id_img_banner_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/id_tv_banner_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center"
        android:layout_marginBottom="25dp"
        android:padding="8dp"
        android:textColor="@android:color/white"
        android:textSize="20sp"
        tools:text="Test"/>
</FrameLayout>

Activity Content 布局

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/id_toolbar_banner_max"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:title="Banner with Max"
        android:background="?attr/colorPrimaryDark"/>

    <ListView
        android:id="@+id/id_listview_banner_max"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

方案一

自定义复合控件容器

public class BannerMax extends FrameLayout implements View.OnClickListener{

}

构建复合组件布局

    private void initUI() {
        View banner = mInflater.inflate(R.layout.layout_banner, this, true);
        mViewPager = (ViewPager) banner.findViewById(R.id.id_viewpager);
        mDotsContainer = (LinearLayout) banner.findViewById(R.id.id_dots_container);

        int count = imgResourcesIds.length;

        for(int i = 0; i < count; i++) {
            ImageView dot = new ImageView(mContext);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT
            );
            params.leftMargin = 5;
            params.rightMargin = 5;
            dot.setImageResource(R.drawable.bg_point_selector);
            if(i == 0)
                dot.setEnabled(true);
            else
                dot.setEnabled(false);
            mDotsContainer.addView(dot, params);
            dots.add(dot);
        }

        for(int i = 0; i < count; i++) {
            View content = mInflater.inflate(R.layout.layout_banner_content, mViewPager, false);
            ImageView img = (ImageView) content.findViewById(R.id.id_img_banner_content);
            TextView tv = (TextView) content.findViewById(R.id.id_tv_banner_content);
            img.setImageResource(imgResourcesIds[i]);
            contents.add(content);
            content.setOnClickListener(this);
        }

        mViewPager.setAdapter(new BannerAdapter());
        int resetIndex = (Integer.MAX_VALUE / 2) - (Integer.MAX_VALUE / 2) % count;
        mViewPager.setCurrentItem(resetIndex);
        mViewPager.addOnPageChangeListener(new BannerPagerChangeListener());
        startShow();
    }

代码分析:首先将Banner布局文件添加到BannerMax这个容器中去。imgResourcesIds是存放了图片资源id的数组,用做Banner的数据源。根据数据源的数量,来创建指示器(dot),添加到Banner布局中的dot容器中;创建ViewPager Item(content),作为ViewPager的数据源。

注意由于Banner布局文件将merge作为根标签,所以使用LayoutInflate加载布局时inflate()方法第三个参数一定为true。

最后设置ViewPager的适配器,滑动监听(OnPageChangeListener),初始化第一item位置,以及开启自动循环动画。

初始化第一个显示的item时,并不是简单的定位到0,而是选择Integer.MAX_VALUE / 2为基点的数据源中第一个item作为第一显示的item。这样做的好处就是当用户打开App后直接向右滑动,仍然可以显示更多的item。如果设置为0,则不可以向右滑动。

自定义Adapter

class BannerAdapter extends PagerAdapter {
        private int dataCount = imgResourcesIds.length;
        @Override
        public int getCount() {
            return Integer.MAX_VALUE;
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {

            return view == object;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(contents.get(position % dataCount));
            return contents.get(position % dataCount);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView(contents.get(position % dataCount));
        }
    }

代码分析:采用第一种方案后,在getCount()返回了一个很大的值,给用户造成无限循环的错觉。这样一来后面两个方法instantiateItem()destroyItem()中的参数position使用起来需要注意,一定要取除以数据源数量的余数,否则将抛出索引越界异常。虽然在getCount()中指定了可用item数量很多,但是通过取模的操作,保证在instantiateItem()中获取到相应的item view 添加到container中。

实现监听器

class BannerPagerChangeListener implements ViewPager.OnPageChangeListener {
        private int dataCount = imgResourcesIds.length;
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            int newPosition = position % dataCount;
            dots.get(newPosition).setEnabled(true);
            dots.get(prePoint).setEnabled(false);
            prePoint = newPosition;
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            switch(state) {
                case ViewPager.SCROLL_STATE_DRAGGING:
                    isAutoPlay = false;
                    break;
                case ViewPager.SCROLL_STATE_IDLE:
                    isAutoPlay = true;
                    break;
                case ViewPager.SCROLL_STATE_SETTLING:
                    isAutoPlay = false;
                    break;
            }
        }
    }

代码分析:可以看到一个类变量isAutoPlay用于决定是否自动滚动item。当ViewPager的状态发生改变时回调onPageScrollStateChanged()方法。用户操作(ViewPager.SCROLL_STATE_DRAGGING)时,将自动滚动关闭;当ViewPager自动校准item完全显示时(ViewPager.SCROLL_STATE_SETTLING),关闭自动滚动;
当ViewPager item停止动画,完全显示后(ViewPager.SCROLL_STATE_IDLE),开启自动滚动。

无论是手动滚动,还是自动滚动,ViewPager在没有结束item动画之前就可以确定要完全显示的item,会调用onPageSelected()方法。在这个方法里面设置指示器,制造跟随图片滚动的效果。

自动循环播放

private Runnable task = new Runnable() {
        @Override
        public void run() {
            int currentItem = mViewPager.getCurrentItem();
            if(isAutoPlay) {
                mViewPager.setCurrentItem(currentItem + 1);
            }
            mHandler.postDelayed(task, delayTime);
        }
};

private void startShow() {
        isAutoPlay = true;
        mHandler.postDelayed(task, delayTime);
}

代码分析:采用Handler机制,利用postDelayed()向队列发送任务,并且延迟执行。由于isAutoPlay的限制,可以很好的解决自动滚动与手动滚动的冲突。

暴露事件处理接口

@Override
    public void onClick(View v) {
        if(mListener != null){
            Info entity = new Info();
            mListener.click(v, entity);
        }
    }

    public interface OnItemClickListener {
        void click(View view, Info entity);
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mListener = listener;
    }

代码分析:在BannerMax中创建了一个接口,用于给调用者实现点击Banner Item事件。

参考

Android自定义控件——仿淘宝、网易、彩票等广告条、Banner的制作

效果

BannerMax.gif

方案二

自定义复合控件容器

public class Banner extends FrameLayout implements View.OnClickListener {
}

构建复合控件布局

private void initUI() {
        //获取Banner布局,并将其添加到FrameLayout中.
        //注意merge标签定义的布局用LayoutInflate加载时第三个参数必须为true
        View view = mInflater.inflate(R.layout.layout_banner, this, true);
        mViewPager = (ViewPager) view.findViewById(R.id.id_viewpager);
        dotsContainer = (LinearLayout) view.findViewById(R.id.id_dots_container);
        dotsContainer.removeAllViews();
        //获取到数据源的数量
        int count = 0;
        if(mEntity != null)
            count = mEntity.size();
        else
            count = imgResourcesId.length;

        /**
         * 根据图片的个数,创建指示器
         */
        for (int i = 0; i < count; i++) {
            ImageView dot = new ImageView(mContext);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT
            );
            params.leftMargin = 5;
            params.rightMargin = 5;
            dot.setImageResource(R.drawable.bg_point_selector);
            if (i == 0) {
                dot.setEnabled(true);
            } else {
                dot.setEnabled(false);
            }
            dotsContainer.addView(dot, params);
            dots.add(dot);
        }

        /**
         * 根据图片的个数,创建ViewPager数据源
         */
        for (int i = 0; i <= count + 1; i++) {
            View content = mInflater.inflate(R.layout.layout_banner_content, null);
            ImageView img = (ImageView) content.findViewById(R.id.id_img_banner_content);
            TextView tv = (TextView) content.findViewById(R.id.id_tv_banner_content);

            if (i == 0) {
                img.setImageResource(imgResourcesId[count - 1]);
            } else if (i == count + 1) {
                img.setImageResource(imgResourcesId[0]);
            } else {
                img.setImageResource(imgResourcesId[i - 1]);
            }
            contents.add(content);
            content.setOnClickListener(this);
        }
        mViewPager.setAdapter(new BannerPagerAdapter());
        mViewPager.setCurrentItem(1);
        currentItem = 1;
        mViewPager.addOnPageChangeListener(new BannerPagerChangeListener());
        startPlay();
    }

代码分析:主要的步骤和方案一类似,但是在创建ViewPager item时还是不一样的。实现方案二原理,在第一个item1的左边放置一个内容和最后一个itemLast相同的itemLeft,在最后一个itemLast右边放置一个内容和第一个item1相同的itemRight。滚动轮回:item1->...->itemLast->itemRight->item1。当从itemRight切换回item1时调用ViewPager.setCurrentItem(1, false)。反之从itemLeft->itemLast一样。

最后和方案一一样设置适配器,监听器,初始化以及开启自动滚动。这里需要注意由于在itemLeft的存在,ViewPager初始化时要定位到position为1的item。

自定义适配器

class BannerPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return contents.size();
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            View content = contents.get((Integer) object);
            return view == content;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(contents.get(position));
            return position;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView(contents.get(position));
        }
    }

代码分析:和一般实现适配器的方法相同。

自定义监听器

class BannerPagerChangeListener implements ViewPager.OnPageChangeListener {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            currentItem = position;
            if(position == imgResourcesId.length + 1) {
                resetDots();
                dots.get(0).setEnabled(true);
            }else if(position == 0) {
                resetDots();
                dots.get(imgResourcesId.length - 1).setEnabled(true);
            }else {
                resetDots();
                dots.get(position - 1).setEnabled(true);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            switch(state) {
                case ViewPager.SCROLL_STATE_DRAGGING:
                    isAutoPlay = false;
                    break;
                case ViewPager.SCROLL_STATE_IDLE:
                    if(currentItem == imgResourcesId.length + 1) {
                        mViewPager.setCurrentItem(1, false);
                    }else if(currentItem == 0) {
                        mViewPager.setCurrentItem(imgResourcesId.length, false);
                    }
                    isAutoPlay = true;
                    break;
                case ViewPager.SCROLL_STATE_SETTLING:
                    isAutoPlay = false;
                    break;
            }
        }
    }

代码分析:这里同样使用一个boolean类变量isAutoPlay来解决手动滑动和自动滚动的冲突。ViewPager.SCROLL_STATE_DRAGGINGViewPager.SCROLL_STATE_SETTLING状态和方案一种的实现一样。主要在ViewPager.SCROLL_STATE_IDLE状态的实现。这里首先判断当前滚动到的item位置,分为以下几种情况:

  1. 当滑动到itemRight时,采用无动画方式切换到position=1的item,开启自动滚动。
  2. 当滑动到itemLeft时,采用无动画方式切换到itemLast,开启自动滚动。
  3. 正常情况,item完全显示,停止动画后,开启自动滚动。

onPageSelected()方法中要将ViewPager选择完全显示的item索引给类变量currentItem,这样方便在onPageScrollStateChanged()中去判断现在处于上述情况中的哪一种,并作出相应处理。

注意调用setCurrentItem(pos, false)时,ViewPager状态并不会改变,所以currentItem一定要在onPageSelected()获取,同时开启自动滚动一定要在ViewPager.SCROLL_STATE_IDLE状态下开启。相反调用setCurrentItem(pos)方法状态会从ViewPager.SCROLL_STATE_SETTLINGViewPager.SCROLL_STATE_IDLE改变。

onPageSelected()还要设置指示器的显示,同样是根据上面三种情况,作出不同的设置。resetDots()方法重置所有dot状态。

/**
 * 初始化所有指示器
 */
private void resetDots() {
    for(int i = 0; i < dots.size(); i++) {
        dots.get(i).setEnabled(false);
    }
}

自动循环播放

    /**
     * 开启自动轮播
     */
    private void startPlay() {
        isAutoPlay = true;
        mHandler.postDelayed(task, delayTime);
    }

    /**
     * 利用Handler处理机制,实现循环轮播
     */
    private final Runnable task = new Runnable() {
        @Override
        public void run() {
            if(isAutoPlay) {
                currentItem += 1;
                mViewPager.setCurrentItem(currentItem);
                mHandler.postDelayed(task,delayTime);
            }else {
                mHandler.postDelayed(task, delayTime);
            }
        }
    };

代码分析:利用Handler不断的向队列发送任务。这里不断的给currentItem增加1,难道不会索引越界吗?当然不会,因为setCurrentItem(pos)方法会改变ViewPager状态,接着调用ViewPager.SCROLL_STATE_IDLE状态实现的方法,最后在onPageSelected()中currentItem重新赋值。

暴露事件处理接口

这个和方案一完全一样...

效果问题

当用户快速的从itemLeft向右滑动,或者从itemRight快速滑动到item1时,会出现些许的卡顿,就像ViewPager滑动到了items的边界。所以有强迫症的话可以选择方案一。

Banner.gif

参考

iKrelve/Kanner

布局优化

布局优化工具HierarchyViewer

不合理的布局,会导致整个应用程序UI启动慢,给用户一种“卡顿”的错觉。如果直接看代码来分析这种“卡顿”现象,很难找到问题出现在哪里。Android SDK给我们提供了一个很好的工具-HierarchyViewer。它能够可视化的直接获取UI布局设计结构和整个ViewTree中View 的属性,堪称UI优化的利器。

打开方式

官方教程建议从Motion中打开HierarchyViewer。

基本使用

  1. 连接模拟器,真机连接时可能无法显示。
  2. 在Window选项卡中选择当前模拟许显示的应用包名(一般为粗体)。
  3. 在TreeView窗口可以看到整个Application的ViewTree。
  4. 在TreeView中选择一个View,会显示一个窗口,详细记录了一些信息:节点的类名,View的id以及它的id名,该节点的Measure,Layout,Draw消耗的时间(包含View以及它的子View)和Measure、Layout、Draw指示器等。同时在左边的View Properties中显示了一些属性。

这里可能遇到一个问题,选择一个View后,Measure、Layout、Draw时间会显示N/A。只需要选择TreeView窗口左上角的Profile Node的选项就可以。记住一定要选择View后在点击Profile Node。

示意图

初始化界面

home_page.png

Window选项卡

window.png

指定View信息窗口

select_iew.png
hv_treeview_screenshot.png

连接真机

Hierarchy Viewer只能连接搭载Android开发版系统的手机或模拟器。 Hierarchy Viewer在连接手机时,手机会启动View Server与其进行Socket通信;但在我们平常用的商业机上,是无法开启View Server的。

检查一台手机是否开启了View Server的方法为:

adb shell service call window 3

  • 若返回值是:Result: Parcel(00000000 00000000 '........'),说明View Server处于关闭状态;
  • 若返回值是:Result: Parcel(00000000 00000001 '........'),说明View Server处于开启状态。

如果要在自己的手机上正常地使用Hierarchy Viewer,有两种方法:

  1. 直接刷一个开发版本的Android固件;
  2. 如果只需要查看自己开发的应用的UI层级,可以用Github上的一个项目ViewServer。

手机端查看布局层次

打开设置—>开发者选项->显示布局边界,可以查看布局结构。

参考

Android UI 优化——使用HierarchyViewer工具

Hierarchy Viewer显示视图性能指标

Optimizing Your UI

include标签

作用:达到布局的复用,方便修改。同时还可以定制特殊要求(修改android:layout_xxx属性)。
使用方法

<include layout="@layout/layout_name"/>

如果需要修改,只需要在layout_name.xml中修改即可,不需要到使用的布局中修改。

include标签中可以覆写所有android:layout_xxx属性。而且会覆盖掉layout_name.xml中根标签中的同名属性。同时还需要注意覆写android:layout_xxx属性时,一定要写android:layout_width&height这两个属性。否则无效。

有一个使用技巧,在include标签中可以定义android:id属性,用于指定复用的布局的根View的id(会覆盖掉复用布局中根View设置的id)。这样在同一个布局中多次使用include复用同一个布局,可以通过include制定不同的id,在代码中获取对应的实例进行操作。如果只使用复用布局的根View的id来获取实例,永远只能获取布局中第一个include标签的实例。

merge标签

作用:防止在引用布局时产生多余的布局嵌套。辅助性扩展include标签。
使用方法

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:id="@+id/ll_dot"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="right"
        android:orientation="horizontal"
        android:padding="8dp" >
    </LinearLayout>

</merge>

好处:Android系统去解析和展示一个布局是需要消耗时间的,布局嵌套的越多,那么解析起来就越耗时,性能也就越差。merge标签的作用就是减少多余的嵌套,可以加快解析,优化性能。

当使用include标签复用一个布局时,该布局最外层的ViewGroup有时候是多余的。有无该ViewGroup都不会影响UI。所以使用merge标签作为改布局文件的根标签,来包含多个View。

当使用LayoutInflate加载以merge为根标签的布局文件时,inflate()的第三个参数必须为true。

这里有一个概念要区分,解析布局和View的绘制是两个操作。解析布局指的是setContentView(),LayoutInflate.inflate()等方法。而View绘制是Measure,Layout,Draw过程。

ViewStub标签

作为初级开发小白的我,很喜欢根据需求,通过设置View的visibility属性来显示和隐藏布局。但是在解析布局文件时,性能很差。因为在初始化解析布局时,也会把那些隐藏的View一一解析出来。
作用:在需要时加载(解析)布局文件,节省初始化解析布局时cpu和内存,提高性能。
使用,在布局文件中添加ViewStub节点

<ViewStub
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

这里看到两个属性:

  • android:layout,用于指定按需加载的布局。
  • android:inflatedId,用于指定按需加载的布局的id。

使用ViewStub标签时,需要设置android:layout_width&height属性,以及android:layout,否则运行报错。

ViewStub优点,它属于View的一种,但是没有大小,没有绘制功能,不参与布局,资源消耗很低。

由于ViewStub是按需加载布局,那么一定是在代码中满足某些特殊情况才加载的,例如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。那么在代码中怎么使用呢?

  1. 通过ViewStub标签的android:id属性获取到ViewStub实例。
  2. 然后调用setVisibility(View.VISIBLE)来解析隐藏的布局。或者使用ViewStub.inflate()方法解析,此方法有一个好处就是会返回一个解析布局的实例,方便再次隐藏,获取其childView实例等操作。

再次隐藏可以设置inflate()返回的对象的Visibility属性来实现。一旦被隐藏的布局解析出来后,ViewStub标签中android:id属性就不可用了,并且ViewStub也不存在在当前的ViewTree中。所以如果要保留隐藏布局的实例可以通过inflate()返回的实力保存,或者后面通过android:inflatedId指定的id来获取实例。

ViewStub属性android:layout指定的布局文件不可以是merge为根标签的布局。

优化经验

LinearLayout or RelativeLayout

虽然建议使用LinearLayout,但是那是在布局层次相同的时候。如果实现同一个布局,使用RelativeLayout比使用LinearLayout布局层次少,那么应该使用RelativeLayout。

多了解SDK中的控件

例如实现一个文字和图片结合的布局,可以使用TextView的drawableLeft/Right属性,而不是使用ViewGroup来包含TextView和ImageView来实现。

少使用layout_weight

使用该属性会减慢View的测量速度。

参考

Android最佳性能实践(四)——布局优化技巧

ANDROID 布局优化

三招优化Android布局

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,085评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,659评论 22 664
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,361评论 2 44
  • 我想拥有一院的花, 不是为我, 不是为你。 我想拥有一院的花, 从春到冬, 从早至晚。 我想拥有一院的花, 灿烂美...
    笔易景悲阅读 241评论 0 2
  • 断断续续的看 每次看就停不下来 感动的点不多 只是不知道为什么最后几集眼泪不曾停过 做自己 真的不容易 不用心去...
    黄小卷阅读 255评论 0 0