Android 手寫一個(gè)輪播圖(banner)框架

話不多說 先看效果:

四種效果Demo

老規(guī)矩 不想聽俺叨逼叨的請(qǐng)移步: GitHub - SuperBanner

首先總結(jié)下需求:
1:支持手指循環(huán)滑動(dòng)
2:支持定時(shí)輪播
3:支持手指觸摸/滑動(dòng)輪播區(qū)域時(shí)停止輪播,手指離開重新輪播
4:支持輪播圖片簡(jiǎn)述及導(dǎo)航(指示器)標(biāo)識(shí)
5:支持圖片點(diǎn)擊事件回調(diào)
6:支持自定義item切換速度
7:支持item圓角圖片展示
8:支持item切換動(dòng)畫(兩種)

關(guān)于ViewPager2

2018 年 9 月 21 日谷歌發(fā)布了首個(gè)AndroidX 穩(wěn)定版本 ----AndroidX 1.0.0。后續(xù)版本中,谷歌意圖用AndroidX逐步替代android.support.xxx 包 那么自然,隸屬于AndroidX下的ViewPager2也將會(huì)替代ViewPager

官方文檔中關(guān)于AndroidX概述

然而就在前幾天(2019年11月20日)ViewPager2也更新了一個(gè)正式穩(wěn)定版ViewPager2 1.0.0

官方文檔關(guān)于ViewPager2的更新及使用方法

不過,考慮到AndroidX的適配問題和現(xiàn)階段的普適程度,此banner效果依然使用ViewPager實(shí)現(xiàn),所以也不打算展開來講ViewPager2,后續(xù)我會(huì)單寫一篇文章詳細(xì)的介紹和使用ViewPager2并實(shí)現(xiàn)此效果,總之,無論用哪種控件實(shí)現(xiàn),思路才最重要。

Google GitHub的ViewPager2 Demo 各位有興趣的可以跑起來先耍耍:
https://github.com/googlesamples/android-viewpager2

進(jìn)入正題

首先我們需要在調(diào)用層(Activity)布局文件中定義出我們自定義的ViewPager相關(guān)布局并設(shè)置一些基本的屬性:

<android.support.v4.view.ViewPager
    android:id="@+id/view_pager"
    android:layout_width="match_parent"
    android:layout_height="180dp" />
<!--指示器的布局-->
<LinearLayout
    android:id="@+id/indicator_ly"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBottom="@id/view_pager"
    android:layout_alignParentRight="true"
    android:layout_marginRight="25dp"
    android:layout_marginBottom="8dp"
    android:orientation="horizontal"></LinearLayout>

ok 這個(gè)時(shí)候還需要一個(gè)Adapter來設(shè)置數(shù)據(jù):

package com.banner.superbanner;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.view.View;
import android.view.ViewGroup;

public class BannerAdapter extends PagerAdapter {

    private BannerBean mBannerBean;
    private OnLoadImageListener mOnLoadImageListener;

    /**
     * @param bannerBean          裝有圖片路徑的數(shù)據(jù)源
     * @param onLoadImageListener 加載圖片的回調(diào)接口 讓調(diào)用層處理加載圖片的邏輯
     */
    private BannerAdapter(BannerBean bannerBean, OnLoadImageListener onLoadImageListener) {
        this.mBannerBean = bannerBean;
        this.mOnLoadImageListener = onLoadImageListener;

    }

    @Override
    public int getCount() {
        return 0;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        return super.instantiateItem(container, position);
    }

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

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
        return view == o;
    }
}

這些個(gè)方法,用過的都知道不多說。接下來主要是在getCount()instantiateItem()中搞事情。

構(gòu)造方法中的BannerBean是我請(qǐng)求服務(wù)器后通過Gson解析后生成的實(shí)體bean,你也可以把圖片組裝到List集合或者arr數(shù)組中 具體還要看你們的業(yè)務(wù)邏輯。

OnLoadImageListener主要是一個(gè)callback接口 主要用于將加載圖片的邏輯回調(diào)給調(diào)用層去處理 這個(gè)后續(xù)會(huì)講到,OnLoadImageListener接口內(nèi)容如下:

package com.banner.superbanner;

import android.content.Context;
import android.widget.View;

public interface OnLoadImageListener {
    //最后一個(gè)參數(shù)類型為View而不是ImageView,主要為了適應(yīng)item布局的多樣性 使用時(shí)強(qiáng)轉(zhuǎn)一下就行了
    void loadImage(Context context, BannerBean bannerBean, int position, View imageView);
}

參數(shù)就不用我多說了吧,看一下基本就明白了,就是加載圖片時(shí)需要的一些信息。

Activity中請(qǐng)求服務(wù)器先把圖片路徑地址拿到:

 OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                .url("http://192.168.0.105:8080/banner/banner_image.json")
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.code() == 200) {
                    Gson gson = new Gson();
                    //Type  type = new TypeToken<BannerBean>(){}.getType();
                    mBannerBean = gson.fromJson(response.body().string(), BannerBean.class);
                    Message message = new Message();
                    message.arg1 = OK;
                    handler.sendMessage(message);
                }

            }
        });

可以看到 請(qǐng)求的host是我的本機(jī)內(nèi)網(wǎng)ip 為了測(cè)試方便 我直接在Tomcat上放了幾張圖片 并且寫了一個(gè)簡(jiǎn)單的json文件模擬服務(wù)器返回的數(shù)據(jù)

請(qǐng)求到的圖片地址如下:

        "http://192.168.0.105:8080/pic/01.png",
        "http://192.168.0.105:8080/pic/02.png",
        "http://192.168.0.105:8080/pic/03.png",
        "http://192.168.0.105:8080/pic/04.png",
        "http://192.168.0.105:8080/pic/05.png",
        "http://192.168.0.105:8080/pic/06.png"

拿到數(shù)據(jù)源后 在PagerAdapterinstantiateItem()中創(chuàng)建ImageView對(duì)象:

 @Override
    public int getCount() {
        return mBannerBean.getData().size();
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        ImageView iv= new ImageView(container.getContext());
        //等比例縮放圖片,占滿容器
        iv.setScaleType(ImageView.ScaleType.FIT_XY);
        if (null!=mOnLoadImageListener){
            //設(shè)置回調(diào),傳入數(shù)據(jù) 讓調(diào)用層(Activity)去處理加載圖片的邏輯
            mOnLoadImageListener.loadImage(container.getContext(),mBannerBean,position,iv);
        }
        //把每一個(gè)item(ImageView)添加到ViewPager容器中
        container.addView(iv);
        return iv;
    }

適配器設(shè)置完畢后 在Activity中給ViewPager添加適配器并加載圖片:

 mViewPager.setAdapter(new BannerAdapter(mBannerBean, new OnLoadImageListener() {
            @Override
            public void loadImage(Context context, BannerBean bannerBean, int position, View imageView) {
                Glide.with(context)
                        .load(bannerBean.getData().get(position))
                        .into((ImageView)imageView);
            }
        }));

此時(shí)運(yùn)行項(xiàng)目:

初步效果演示

可以看到 總共6張圖片 在我滑動(dòng)到最后一張的時(shí)候 我們需要讓它繼續(xù)從頭開始循環(huán)滑動(dòng)。

手指滑動(dòng)“無限循環(huán)”

這里就要說到ViewPagerAdpater中的getCount()函數(shù),這個(gè)函數(shù)的返回值就是當(dāng)前ViewPager的總頁數(shù)(item),當(dāng)ViewPager滑動(dòng)到最后一頁 也就是當(dāng)前item的position為getCount()-1的時(shí)候 就會(huì)認(rèn)為已經(jīng)滑動(dòng)到了末尾。

所以,我們這里所說的無限循環(huán)滑動(dòng)其實(shí)是一個(gè)偽概念 因?yàn)槲覀償?shù)據(jù)源的總大小也才6張圖片 等我們滑到第5個(gè)item的時(shí)候理論上已經(jīng)滑不動(dòng)了 但為了做出無限循環(huán)效果,我們可以給getCount()返回一個(gè)非常大的數(shù) 讓它很難滑動(dòng)到盡頭。

比較主流的做法是直接返回Interger的最大值:

 @Override
    public int getCount() {
        //return mBannerBean.getData().size();
        return Integer.MAX_VALUE; //返回Integer的最大值,實(shí)現(xiàn)“手指滑動(dòng)無限循環(huán)”
    }
Integer最大值SDK文檔解釋

如圖 ,MAX_VALUE的值為: 2的31次方減1 得出的一個(gè)常量值:2147483647,換句話說 理論上你需要滑動(dòng)二十一億四千七百四十八萬三千六百四十七次才能滑動(dòng)到盡頭....


想必世界上應(yīng)該還沒有如此耿直的人非要滑那么多次的吧 那么 它就是“無限循環(huán)”

或者你還可以這樣寫:

 @Override
    public int getCount() {
        //return mBannerBean.getData().size();
        //返回?cái)?shù)據(jù)源大小的整數(shù)倍
        return (mBannerBean.getData().size() * 10000 * 100);
    }

這種是直接返回?cái)?shù)據(jù)源的整數(shù)倍的方式,個(gè)人推薦這種寫法 原因后續(xù)會(huì)講到。反正不管怎么寫 核心就是返回一個(gè)非常大的數(shù) 在相當(dāng)長(zhǎng)的時(shí)間內(nèi)滑不到盡頭。

ps: 關(guān)于無限循環(huán) 市面上還有一些其他做法 比如重寫 OnPageChangeListener 接口中的onPageSelected 方法或者我看有些人通過動(dòng)態(tài)添加/復(fù)用頭尾item的方式做到所謂“真正意義上的無線循環(huán)”,有興趣請(qǐng)自行瀏覽器了解

ok 我們?cè)O(shè)置完getCount()返回值后,此時(shí)我們?nèi)绻苯舆\(yùn)行項(xiàng)目 會(huì)報(bào)出IndexOutOfBoundsException異常,其原因在于:我們?cè)O(shè)置了ViewPager的item的總大小但并沒有對(duì)position進(jìn)行處理,當(dāng)postion的值超出了數(shù)據(jù)源(list集合)的大小 就會(huì)拋出索引越界異常

所以當(dāng)前的position如果超出我們數(shù)據(jù)源的最大值(最大值為6) 我們需要把這個(gè)position處理成數(shù)據(jù)源范圍內(nèi)的值:

 @NonNull
    @Override
    private Object instantiateItem(@NonNull ViewGroup container, int position) {
        ImageView iv= new ImageView(container.getContext());
        iv.setScaleType(ImageView.ScaleType.FIT_XY);
        Log.i("TEST_POSITION","處理之前的position: "+position);
        //處理position 通過取余數(shù)的方式來限定position的取值范圍
        position = position % mBannerBean.getData().size();
        Log.i("TEST_POSITION","處理之后的position:"+position);
        if (null!=mOnLoadImageListener){
            //設(shè)置回調(diào),傳入數(shù)據(jù) 讓調(diào)用層(Activity)去處理加載圖片的邏輯
            mOnLoadImageListener.loadImage(container.getContext(),mBannerBean,position,iv);
        }
        //把每一個(gè)item(ImageView)添加到ViewPager容器中
        container.addView(iv);
        return iv;
    }

剛好 我們可以通過取余數(shù)的特性 限定position的取值范圍: 從0到數(shù)據(jù)源大小-1之間
此時(shí)運(yùn)行項(xiàng)目并打印position日志:

可以看到 已經(jīng)可以無限的向右滑動(dòng)了,我向右滑動(dòng)了兩輪 此時(shí)Log打印出position值為:


position處理前與處理后的值

看到了8~ 如果沒處理position 當(dāng)position為6的時(shí)候 就已經(jīng)索引越界了。我們通過取余處理后 position值就能按順序控制在0-6之間以此類推

ok 看似已經(jīng)實(shí)現(xiàn)了手指滑動(dòng)無限循環(huán) 但有一個(gè)小問題 我向右滑動(dòng)沒問題 但我向左邊滑動(dòng)到position值為0的item的時(shí)候就滑不動(dòng)了,ViewPager就會(huì)認(rèn)為我左邊已經(jīng)沒有item了。

解決這個(gè)問題 只需要讓ViewPager左滑時(shí) 在相當(dāng)長(zhǎng)的時(shí)間內(nèi)滑不到0的位置

很簡(jiǎn)單,ViewPager中有一個(gè)API:


官方文檔API解釋

Set the currently selected page. If the ViewPager has already been through its first layout with its current adapter there will be a smooth animated transition between the current item and the specified item.
設(shè)置當(dāng)前選擇的頁面。如果ViewPager已經(jīng)使用當(dāng)前適配器完成了它的第一個(gè)布局,那么當(dāng)前項(xiàng)和指定項(xiàng)之間將有一個(gè)平滑的動(dòng)畫過渡。

一般情況下ViewPager初始化時(shí)默認(rèn)的item位置為0。

但我們可以使用這個(gè)API給ViewPager一個(gè)初始位置:

//ViewPager初始化時(shí) 滑動(dòng)到一半的距離
mViewPager.setCurrentItem((mViewPager.getAdapter().getCount()) / 2);

在初始化的時(shí)候 給ViewPager設(shè)置初始位置為:總條目數(shù)的一半

這樣一來 不論是左滑還是右滑都不會(huì)滑到"盡頭"

但問題來了 還記得剛剛提到的實(shí)現(xiàn)無限循環(huán)在getcount()中的兩種返回方式的寫法嗎? 一個(gè)是返回Integer最大值 一種是返回?cái)?shù)據(jù)源的整數(shù)倍,并且我還推薦使用整數(shù)倍的寫法。

如果你使用的是返回Integer最大值的方式:

你會(huì)發(fā)現(xiàn)當(dāng)你冷啟動(dòng)App時(shí) ViewPager顯示的item位置經(jīng)過取余處理后 仍然不會(huì)在第一位,一般情況下 我們正常需求肯定都是初始化顯示第一張圖片(position = 0) 為什么會(huì)出現(xiàn)這種情況呢?

原因就在于:這個(gè)數(shù)不能被整除

所以你還要對(duì)它的余數(shù)進(jìn)行拼差處理, 太麻煩了 而且這個(gè)數(shù)也太大 我們沒必要設(shè)置這么大的數(shù)。

所以個(gè)人推薦使用整數(shù)倍的方式 。

定時(shí)輪播

在Android中 想要周期性執(zhí)行任務(wù)基本有以下幾種方式:

  • Timer+TimerTask
  • 延時(shí)Handler(postDelay)
  • 周期性執(zhí)行任務(wù)的線程池

首先pass掉第三種 不解釋 第一種和第二種用哪個(gè)都可以 很多人在用postDelay的方式 那咱們就用Timer+TimerTask吧:

Acitivity類中:

    private Timer mTimer;
    private TimerTask mTimerTask;
    /**
     * 開啟一個(gè)延時(shí)任務(wù)并執(zhí)行
     */
    private void executeDelayedTask() {
        //在創(chuàng)建任務(wù)之前 一定要檢查清理未回收的任務(wù),保證只有一組Timer+TimerTask
        killDelayedTask();
        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        //顯示下一頁
                        showNextPage();
                    }
                });
            }
        };
     //設(shè)置delay參數(shù)為3000毫秒表示用戶調(diào)用schedule() 方法后,要等待3秒才可以第一次執(zhí)行run()方法
     //設(shè)置period參數(shù)為4000 表示第一次調(diào)用之后,從第二次開始每隔4秒調(diào)用一次run()方法
        mTimer.schedule(mTimerTask, 3000, 4000);
    }

    /**
     * @Description 取消(清理)延時(shí)任務(wù)
     */
    private void killDelayedTask() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    /**
     * @Description 顯示下一頁
     */
    private void showNextPage() {
        //獲取到當(dāng)前頁面的位置
        int currentPageLocation = mViewPager.getCurrentItem();
        //設(shè)置item位置為: 當(dāng)前頁面的位置+1
        mViewPager.setCurrentItem(currentPageLocation + 1);
    }

如上 使用Timer+TimerTask 執(zhí)行定時(shí)任務(wù) 這個(gè)任務(wù)就是: showNextPage()顯示下一頁。

手指觸摸/滑動(dòng)輪播區(qū)域時(shí)停止輪播,手指離開重新輪播

這個(gè)也很簡(jiǎn)單 只需要用到ViewPager的一個(gè)API

依舊是在Acivity類中:

  /**
      * @Description 在手指按下和移動(dòng)時(shí) 清除延時(shí)任務(wù),待手指松開重新創(chuàng)建任務(wù)
     */
    private void setViewPagerTouchListener() {
        mViewPager.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        killDelayedTask();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        killDelayedTask();
                        break;
                    case MotionEvent.ACTION_UP:
                        executeDelayedTask(mDelay,mDelay);
                        break;
                }
                return false;
            }
        });
    }

ActivityonCreate()中初始化一下:

//設(shè)置3秒鐘后開始執(zhí)行任務(wù)  每個(gè)任務(wù)之間隔4秒執(zhí)行一次
 superBanner.executeDelayedTask();
//初始化touch事件
 superBanner.setViewPagerTouchListener();

此時(shí)運(yùn)行項(xiàng)目:


定時(shí)輪播+手指觸停

注意看 我手指觸摸滑動(dòng)的時(shí)候 此時(shí)會(huì)停止輪播 當(dāng)手指松開后 又會(huì)繼續(xù)輪播。

底部指示器:

定時(shí)輪播完成后 我們想在ViewPager底部顯示一排"指示器",可以隨頁面的滑動(dòng)更改狀態(tài)

依然實(shí)在Activity類中:

  /**
     * @Description 初始化ViewPager底部指示器
     * @param indicatorLayout 指示器的父布局 由調(diào)用者提供
     */
    public void initIndicatorView(Context context, BannerBean bannerBean, ViewGroup indicatorLayout) {
        this.mIndicatorLayout = indicatorLayout;
        for (int i = 0; i < bannerBean.getData().size(); i++) {
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dpToPx(6), dpToPx(6));
            lp.leftMargin = dpToPx(10);
            lp.bottomMargin = dpToPx(6);

            View ivIndicator = new View(context);
            //[R.drawable.indicator_select]為指示器的背景資源 相關(guān)樣式可替換
            ivIndicator.setBackgroundResource(R.drawable.indicator_select);
            ivIndicator.setLayoutParams(lp);
            //將一個(gè)個(gè)指示器(ImageView)添加到父布局中
            indicatorLayout.addView(ivIndicator);
        }

    }

上述代碼段中的dpToPx()作用是將dp值轉(zhuǎn)換為像素值 想必大多人項(xiàng)目的Util中應(yīng)該有該方法, 還是貼出來吧:

/**
     * @Description 將dp轉(zhuǎn)為px
     */
    private int dpToPx(int dp) {
        //獲取手機(jī)屏幕像素密度
        float phoneDensity = getResources().getDisplayMetrics().density;
        //加0.5f是為了四舍五入 避免丟失精度
        return (int) (dp * phoneDensity + 0.5f);

    }

指示器創(chuàng)建完畢后,需要將指示器中的每個(gè)view與頁面切換/選中狀態(tài)捆綁:

/**
     * @Description 隨著ViewPager頁面滑動(dòng) 更新指示器選中狀態(tài)
     * @param position ViewPager中的item的position
     */
    public void updateIndicatorSelectState(int position) {
        //此時(shí)傳入的position還未經(jīng)過處理 同樣的需要對(duì)position進(jìn)行取余數(shù)處理
        position = position % mIndicatorLayout.getChildCount();
        //循環(huán)獲取指示器父布局中所有的子View
        for (int i = 0; i < mIndicatorLayout.getChildCount(); i++) {
            //給每個(gè)子view設(shè)置選中狀態(tài)
            //當(dāng)i == position為True的時(shí)候觸發(fā)選中狀態(tài)反之則設(shè)置成未選中
            mIndicatorLayout.getChildAt(i).setSelected(i == position);

        }
    }

如上述代碼段,updateIndicatorSelectState()需要接受一個(gè)position , 那么這個(gè)position從哪里來?換句話說,該在何時(shí)調(diào)用此方法?

沒錯(cuò) 那就是需要在ViewPager頁面狀態(tài)發(fā)生改變時(shí)調(diào)用。所以還要給ViewPager添加一個(gè)頁面狀態(tài)事件監(jiān)聽:

   /**
     *@Description 添加ViewPager頁面改變事件的監(jiān)聽
     */
    public void initPageChangeListener(){
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float v, int position1) {

            }

            @Override
            public void onPageSelected(int position) {
                //更新指示器選中狀態(tài)
                updateIndicatorSelectState(position);
            }

            @Override
            public void onPageScrollStateChanged(int position) {

            }
        });
    }

三個(gè)回調(diào)方法想必大家都很熟悉了吧,不解釋,初始化后 然后運(yùn)行項(xiàng)目:

            //初始化指示器
            initIndicatorView();
            //在初始化的時(shí)候 讓指示器選中第一個(gè)位置
            updateIndicatorSelectState(0);
            //初始化ViewPager頁面選擇狀態(tài)監(jiān)聽
            initPageChangeListener();
無限循環(huán)+自動(dòng)輪播+觸開離停+底部指示器

至此,我們的基礎(chǔ)的業(yè)務(wù)功能已經(jīng)實(shí)現(xiàn)。

但是, 這UI樣式有些過時(shí)而且頁面切換的時(shí)候交互略顯生硬不夠 優(yōu)雅

那好 接下來咱們就著手讓它盡可能好看一點(diǎn)

動(dòng)畫效果及UI美化

想要好看 肯定是要改變UI樣式或者添加動(dòng)畫。

首先ViewPager中的圖片都是直角 太直了不好看 聽說流行圓角好多年了 那咋辦? 先把ImageView剪裁成圓角再說:

package com.banner.superbanner;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.os.Build;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
/**
 *@Description 通過繪出一個(gè)圓角矩形的路徑,然后用ClipPath裁剪畫布的方式對(duì)ImageView的邊角進(jìn)行剪裁實(shí)現(xiàn)圓角
 */
public class CircularBeadImageView extends AppCompatImageView {
    float width,height;
    //此值代表圓角的半徑
    int angle = 30;

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

    public CircularBeadImageView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public CircularBeadImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
  //Android4.0及之前的手機(jī)中,因?yàn)橛布铀俚仍颍谑褂胏lipPath時(shí)很有可能 會(huì)發(fā)生UnsupportedOperationException異常
        if (Build.VERSION.SDK_INT < 18) {
            setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        } 
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        width = getWidth();
        height = getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
    //主要為了防止屏幕寬高小于圓角半徑值這種詭異的現(xiàn)象出現(xiàn)
        if (width > angle && height > angle) {
            Path path = new Path();
            path.moveTo(angle, 0);
            path.lineTo(width - angle, 0);
            path.quadTo(width, 0, width, angle);
            path.lineTo(width, height - angle);
            path.quadTo(width, height, width - angle, height);
            path.lineTo(angle, height);
            path.quadTo(0, height, 0, height - angle);
            path.lineTo(0, angle);
            path.quadTo(0, 0, 40, 0);
            canvas.clipPath(path);
        }
         super.onDraw(canvas);
    }
}

項(xiàng)目res/layout文件夾下增加布局: item_ly.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/iv_ly"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <com.banner.superbanner.CircularBeadImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        />
</RelativeLayout>

ViewPager的item的布局是在BannerAdapter中instantiateItem()中創(chuàng)建的,改一下:

給item添加布局

都是日常操作,不解釋。不過俗話講:空白留有余韻,所以唯一要注意的是 我給item布局設(shè)置了一個(gè)padding值 這樣我們的item就可以距離父控件上下左右有些距離 這樣視覺上會(huì)更好看

不信看下效果:

內(nèi)邊距+圓角

這個(gè)稍微岔個(gè)話,關(guān)于IamgeView圓角的實(shí)現(xiàn)方式有很多 關(guān)于ViewPager item圓角的方式也有很多,比如你們?nèi)绻?code>Glide圖片加載框架 就可以通過重寫Glide自帶的加載器直接給ImageView加載圓角,這樣就不用再單給item寫一套布局了(其他圖片框架基本也都支持)。

舉個(gè)栗子?:

 RequestOptions options = RequestOptions.bitmapTransform(new CenterCropRoundCornerTransform(20));
        Glide.with(container.getContext())
                .load(mBannerBean.getData().get(position))
                .apply(options)
                .into(cb_iv);

CenterCropRoundCornerTransform類是繼承并重寫了Glide專門讓我們加載圓角圖片的CenterCrop類:

package com.banner.superbanner;

import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;

import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;

import java.security.MessageDigest;

public class CenterCropRoundCornerTransform extends CenterCrop {
    private static float radius = 0f;
    /**
      *構(gòu)造中接受圓角半徑參數(shù)
    */
    public CenterCropRoundCornerTransform(int px) {
        this.radius = px;
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        Bitmap bitmap = TransformationUtils.centerCrop(pool, toTransform, outWidth, outHeight);
        return roundCrop(pool, bitmap);
    }

    private static Bitmap roundCrop(BitmapPool pool, Bitmap source) {
        if (source == null) return null;

        Bitmap result = pool.get(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_4444);
        if (result == null) {
            result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_4444);
        }

        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
        paint.setAntiAlias(true);
        RectF rectF = new RectF(0f, 0f, source.getWidth(), source.getHeight());
        canvas.drawRoundRect(rectF, radius, radius, paint);
        return result;
    }

    public String getId() {
        return getClass().getName() + Math.round(radius);
    }

    @Override
    public void updateDiskCacheKey(MessageDigest messageDigest) {

    }
}

OK了, 就是這么簡(jiǎn)單。

回到正題, UI樣式是修改了 但是item自動(dòng)切換的時(shí)候 依舊感覺很生硬...

其實(shí)感覺到“生硬”是因?yàn)榍袚Q的時(shí)候速度太快 一瞬而過 不夠平滑,這個(gè)問題可以通過修改item切換速度來解決。

但是ViewPager的item切換速度是寫死的 并沒有暴露出API讓我們修改,我們只能通過反射的方式去修改切換速度。

ViewPager的切換速度是通過Scroll類來控制的,新建SuperBannerScroller類重寫它:

package com.banner.superbanner;

import android.content.Context;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class SuperBannerScroller extends Scroller {
    //切換動(dòng)畫時(shí)長(zhǎng)(單位:毫秒)
    private int mScrollDuration = 2000; 
    private static final Interpolator sInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };
    public boolean noDuration;
   /**
    *此方法主要讓調(diào)用層控制是否延時(shí)
   */
    public void setNoDuration(boolean noDuration) {
        this.noDuration = noDuration;
    }

    public SuperBannerScroller(Context context) {
        this(context, sInterpolator);
    }

    public SuperBannerScroller(Context context, Interpolator interpolator) {
        super(context, interpolator);
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        if (noDuration) {
            super.startScroll(startX, startY, dx, dy, 0);
        } else {
            //默認(rèn)延時(shí)
            super.startScroll(startX, startY, dx, dy, mScrollDuration);
        }
    }
}

重寫完之后 只需要在初始化ViewPager的時(shí)候 反射到具體的參數(shù) 然后替換一下:

/**
     * @Description 通過反射的方式拿到ViewPager的mScroller,然后替換成自己設(shè)置的值
     */
    private void updateViewPagerScroller() {
        mSuperBannerScroller = new SuperBannerScroller(this);
        Class<ViewPager> cl = ViewPager.class;
        try {
            Field field = cl.getDeclaredField("mScroller");
            field.setAccessible(true);
            //利用反射設(shè)置mScroller域?yàn)樽约憾x的MScroller
            field.set(mViewPager, mSuperBannerScroller);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

初始化調(diào)用一下這個(gè)方法,然后運(yùn)行看下效果:

item切換延時(shí)兩秒+內(nèi)邊距

我為了讓效果稍微直觀一點(diǎn),我設(shè)置切換速度為2秒,其實(shí)一秒鐘就已經(jīng)ok了,看起來有木有舒服些?

ps:目前大多數(shù)主流App, 包括但不限于:淘寶、網(wǎng)易云音樂、掌上生活(招行)、華為應(yīng)用市場(chǎng)、優(yōu)酷、京東..等等的banner基本上都是item圓角或者內(nèi)邊距的形式顯示。此Demo保留了圖片加載由調(diào)用層處理的回調(diào) 你們可以自由加載。

UI樣式告一段落,下面開始加動(dòng)畫。

ViewPager有個(gè)API:

官方文檔API說明

API功能翻譯:設(shè)置viewpage。當(dāng)滾動(dòng)位置改變時(shí),將為每個(gè)附加頁調(diào)用PageTransformer。這允許應(yīng)用程序?qū)γ總€(gè)頁面應(yīng)用自定義屬性轉(zhuǎn)換,覆蓋默認(rèn)的滑動(dòng)行為。
API參數(shù)翻譯:
reverseDrawingOrder:------ 布爾值:如果提供的PageTransformer要求從最后到第一而不是從第一到最后繪制頁面視圖,則為真。
transformer------ PageTransformer將修改每個(gè)頁面的動(dòng)畫屬性
pageLayerType ------ 應(yīng)用于ViewPager頁面的視圖層類型。它應(yīng)該是LAYER_TYPE_HARDWARE、LAYER_TYPE_SOFTWARE或LAYER_TYPE_NONE。

說白了就是可以利用這個(gè)API給ViewPager添加頁面切換動(dòng)畫效果。看下它的源碼:

public void setPageTransformer(boolean reverseDrawingOrder, @Nullable ViewPager.PageTransformer transformer, int pageLayerType) {
        boolean hasTransformer = transformer != null;
        boolean needsPopulate = hasTransformer != (this.mPageTransformer != null);
        this.mPageTransformer = transformer;
        this.setChildrenDrawingOrderEnabled(hasTransformer);
        if (hasTransformer) {
            this.mDrawingOrder = reverseDrawingOrder ? 2 : 1;
            this.mPageTransformerLayerType = pageLayerType;
        } else {
            this.mDrawingOrder = 0;
        }

        if (needsPopulate) {
            this.populate();
        }

    }

這是個(gè)重載方法 文檔結(jié)合源碼 首先這個(gè)方法接收三個(gè)參數(shù),第一個(gè)參數(shù)和最后一個(gè)參數(shù)不是重點(diǎn),自行理解,關(guān)鍵是PageTransformer這個(gè)參數(shù)。

PageTransforme是個(gè)啥玩意兒呢:

谷歌開發(fā)指南中的解釋

注意圖中標(biāo)注區(qū)域,一定要搞清楚這些解釋的真正含義 才能自定義各種動(dòng)畫,如果還沒用過PageTransforme自行瀏覽器了解 這里就不深入講了......

為啥不講了? 因?yàn)槲遥?/p>

哈哈哈開玩笑啦 其實(shí)是因?yàn)?code>PageTransforme這個(gè)東西細(xì)節(jié)太多 如果想完全講清楚 需要占用大量篇幅,完全可以單寫一篇文章詳細(xì)講解了。網(wǎng)上有大量相關(guān)PageTransforme的文章講解 如果不太清楚PageTransforme的你們就自行了解吧

但本文會(huì)使用谷歌開發(fā)指南中ViewPager的兩個(gè)動(dòng)畫例子 來給我們的ViewPager加上動(dòng)畫效果。

第一個(gè):


DepthPageTransformer頁面深度線性淡出效果

DepthPageTransformer官方示例效果:


DepthPageTransformer效果

怎么實(shí)現(xiàn)?很簡(jiǎn)單啊 谷歌demo示例代碼都給咱寫好了:

public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1f);
            view.setTranslationX(0f);
            view.setScaleX(1f);
            view.setScaleY(1f);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}

把這個(gè)類copy到你項(xiàng)目中,然后在初始化ViewPager的時(shí)候調(diào)用:

            //第一個(gè)參數(shù)為true表示頁面是按正序添加 反之則為倒序。(一般只有在幀布局的時(shí)候才有視覺效果)
            //第二個(gè)參數(shù)為具體的動(dòng)畫樣式的實(shí)例,此方法一定要在setAdapter之前調(diào)用!!!!
             mViewPager.setPageTransformer(true, new DepthPageTransformer());

看下效果:


DepthPageTransformer實(shí)例效果

第二個(gè):


ZoomOutPageTransformer收縮淡入效果

ZoomOutPageTransformer官方示例效果:


ZoomOutPageTransformer效果

同樣的 這個(gè)Demo的示例代碼谷歌也給了我們:

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 1) { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA +
                    (scaleFactor - MIN_SCALE) /
                    (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}

不多說 直接初始化:

mViewPager.setPageTransformer(true, new ZoomOutPageTransformer());

看下效果:


ZoomOutPageTransformer效果實(shí)現(xiàn)

這塊還可以更美化一點(diǎn) 比如說我們經(jīng)常見到的3D畫廊效果,一屏可以顯示多頁 然后縮放漸入。縮放漸入我們實(shí)現(xiàn)了 怎么能一屏顯示多頁呢?

別想那么復(fù)雜, 一個(gè)屬性就能搞定:

clipChildren屬性文檔解釋

定義子對(duì)象是否被限制在其界限內(nèi)繪制。這對(duì)于將孩子的大小縮放到100%以上的動(dòng)畫非常有用。在這種情況下,應(yīng)該將此屬性設(shè)置為false,以允許子元素繪制超出其邊界的內(nèi)容。此屬性的默認(rèn)值為true。
可以是一個(gè)布爾值,如“true”或“false”。

這是ViewGroup中的一個(gè)特有屬性 它可以允許子控件越界顯示

在布局的根布局中設(shè)置一下:


一屏顯示多頁布局設(shè)置

如圖,在根布局加上android:clipChildren="false" 然后我把ViewPager的寬度由原來的match_parent改成指定寬度 這樣做是為了讓其不要填滿窗體 這樣其它頁面才能展示到當(dāng)前屏幕上(實(shí)際使用時(shí) 這個(gè)值不要隨便給,最好是通過機(jī)型屏幕寬度計(jì)算出的一個(gè)寬度值)。

直接運(yùn)行項(xiàng)目看下效果:


3D畫廊效果

當(dāng)然 你們也可以自定義動(dòng)畫效果,包括動(dòng)畫的具體參數(shù) 比如透明度 縮放比 樣式等等參數(shù) 都可以自己調(diào)整。

奧 差點(diǎn)忘了,我們還沒有給`ViewPager添加item點(diǎn)擊事件。

設(shè)置item點(diǎn)擊事件

由于ViewPager并沒有直接提供點(diǎn)擊事件的API 所以目前有很多種方式給ViewPager添加點(diǎn)擊事件 比如在touch中通過對(duì)手勢(shì)事件的攔截和偏移量的計(jì)算,還有直接給item的View添加點(diǎn)擊事件 然后再回調(diào)給ViewPager。那我們就采用第二種方案。
首先定義出一個(gè)回調(diào)接口:

package com.banner.superbanner.callback;

/**
  *@Description ViewPager item的點(diǎn)擊事件
  *
 */
public interface OnItemClickAdapterListener {
    void onItemAdapterClick(int position);
}

這個(gè)callback主要用于BannerAdapter類中,在BannerAdapter的構(gòu)造方法中接受回調(diào)的實(shí)例,然后在instantiateItem()中觸發(fā)回調(diào):

 /**
     *@Description item的View的點(diǎn)擊事件
     * @param cb_iv    點(diǎn)擊事件的view
     * @param position ietm的索引(取余過后的)
     */
    private void onItemClick(CircularBeadImageView cb_iv, final int position) {
        if (cb_iv != null) {
            cb_iv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnItemClickAdapterListener != null) {
                        mOnItemClickAdapterListener.onItemAdapterClick(position);
                    }
                }
            });
        }
    }

我把這塊邏輯抽了出來,單獨(dú)寫了個(gè)方法 這個(gè)方法只需要在instantiateItem()中調(diào)用一下就可以了,然后在Acitivity中給ViewPager設(shè)置Adapter時(shí) 在BannerAdapter的構(gòu)造中實(shí)現(xiàn)OnItemClickAdapterListener回調(diào) 重寫onItemAdapterClick()就OK了。

代碼整合與封裝

代碼寫到這里 我們想要的需求也全部實(shí)現(xiàn) 但代碼結(jié)構(gòu)亂的一批。

一方面為了演示方便和自己方便 我直接把ViewPager相關(guān)代碼全部寫在了調(diào)用層Activity中,可讀性不強(qiáng)。

第二方面 ViewPager業(yè)務(wù)邏輯的具體參數(shù)直接寫死了 沒有提供讓外部賦值的方法,不利于擴(kuò)展。

第三方面 沒有對(duì)實(shí)例進(jìn)行非空校驗(yàn) 沒有對(duì)代碼進(jìn)行容錯(cuò)考慮。

這三個(gè)方面 就造成了一個(gè)問題: 耦合嚴(yán)重,健壯性差

良好的代碼結(jié)構(gòu)應(yīng)該是: 高內(nèi)聚 低耦合 。調(diào)用層和實(shí)現(xiàn)層要盡量解耦

下面要做一些封裝和抽取 目的就是: 要把所有和ViewPager的相關(guān)的業(yè)務(wù)邏輯內(nèi)聚到一個(gè)類中并對(duì)外暴露API 讓調(diào)用層決定banner業(yè)務(wù)邏輯中的具體參數(shù) 并封裝成一個(gè)簡(jiǎn)易框架。

需求是,調(diào)用者可以決定banner:

  • 是否可以手指滑動(dòng)無限循環(huán)
  • 是否可以定時(shí)輪播
  • 是否擁有底部指示器
  • 是否擁有動(dòng)畫效果
  • 是否需要item點(diǎn)擊事件
  • 是否需要調(diào)用者去處理加載圖片的邏輯
  • 是否需要item圓角展示(glide)
  • Banner頁面切換的速度
  • 定時(shí)輪播的間隔時(shí)間
  • 底部指示器View的寬高和間距(相對(duì)于父布局)
  • item的內(nèi)邊距

新建一個(gè) SuperBanner類 將之前寫在Activity中和ViewPager相關(guān)的代碼全部移植到此類中,然后將上述需求整理成具體函數(shù),以方法鏈的形式暴露出去,最終調(diào)用層對(duì)Banner的設(shè)置只需要以下API:

這樣一來,調(diào)用者只需要確定上述的一些參數(shù) banner的實(shí)現(xiàn)就和調(diào)用層無關(guān)了

由于 SuperBanner類的代碼太多,貼出來太影響閱讀體驗(yàn) 如果感興趣請(qǐng)自行下載Demo了解 注釋都很詳細(xì) 。

使用

 private void showTest(){
        //簡(jiǎn)單用法
        mSuperBanner.setDataOrigin(imageList).start();
    }

你可以直接設(shè)置數(shù)據(jù)源 然后start 但是這樣沒有底部指示器 也沒有其他的一些效果 但默認(rèn)會(huì)有自動(dòng)輪播和手指滑動(dòng)無限輪播效果。

全部API:

 mSuperBanner.
                //設(shè)置數(shù)據(jù)源
                 setDataOrigin(imageList)
                //重載方法,設(shè)置指示器布局及指示器樣式,不需要就無需調(diào)用 后三個(gè)參數(shù)代表指示器的寬高和間距(可選設(shè)置 有默認(rèn)效果)
                .setIndicatorLayoutParam(mIndicatorLayout, R.drawable.indicator_select, 6, 6, 10)
                //設(shè)置ViewPager的item切換速度,不需要更改速度就無需調(diào)用
                .setViewPagerScroller(1000)
                //設(shè)置自動(dòng)輪播間隔時(shí)間,重載方法 默認(rèn)開始執(zhí)行定時(shí)任務(wù)時(shí)間為2秒
                .setAutoIntervalTime(3000, 2000)
                //.closeAutoBanner(true)  關(guān)閉自動(dòng)輪播
                //.closeInfiniteSlide(true)  關(guān)閉手指滑動(dòng)無限循環(huán)
                //設(shè)置item的padding值(上下左右)
                .setItemPadding(14)
                //設(shè)置圓角半徑 一旦設(shè)置值(大于0) 就代表item使用圓角樣式
                .setRoundRadius(10)
                //.setSwitchAnimation()  設(shè)置ViewPager切換動(dòng)畫

                //可選實(shí)現(xiàn)。實(shí)現(xiàn)圖片加載回調(diào)(一定要在start()之前執(zhí)行) 一但實(shí)現(xiàn)回調(diào)就表示圖片加載交由調(diào)用層處理 否則由適配器內(nèi)部加載
                .setOnLoadImageListener(new SuperBanner.OnLoadImageListener() {
                    @Override
                    public void onLoadImage(List imageData, int position, View imageView) {
                        if (mOptions == null) {
                            mOptions = RequestOptions.bitmapTransform(new CenterCropRoundCornerTransform(20));
                        }
                        int resourceId = (int) imageData.get(position);
                        Glide.with(MainActivity.this)
                                .load(resourceId)
                                .apply(mOptions)
                                .into((ImageView) imageView);
                    }
                })

                // 可選實(shí)現(xiàn)。實(shí)現(xiàn)item點(diǎn)擊事件回調(diào)(一定要在start()之前執(zhí)行)
                .setOnItemClickListener(new SuperBanner.OnItemClickListener() {
                    @Override
                    public void onItemClick(int position) {
                        Log.i("BannerItemPosition: ", position + "");
                    }
                })
                // 此函數(shù)要最后執(zhí)行
                .start();

如果有其他需求 直接改源碼就ok 注釋真的很詳細(xì)奧~

奧,最后 你可以在Activity/Fragment不可見的時(shí)候 關(guān)掉輪播定時(shí)任務(wù)以盡可能的減少內(nèi)存壓力和內(nèi)存泄漏發(fā)生:

 @Override
    protected void onPause() {
        super.onPause();
        //取消輪播
        if (mSuperBanner!=null){
            mSuperBanner.killDelayedTask();
        }
    }

然后在頁面可見時(shí)開啟:

 @Override
    protected void onStart() {
        super.onStart();
        //開始輪播
        if (mSuperBanner!=null){
            mSuperBanner.executeDelayedTask();
        }
    }

很希望能幫到你 不足之處還請(qǐng)見諒 懇請(qǐng)斧正 !。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

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