仿寫一個(gè)QQ空間圖片預(yù)覽Dialog

前言

彈幕除了能用來做直播,還能用來做什么?如果你看過QQ空間,你肯定知道,QQ空間的圖片預(yù)覽使用了彈幕。今天,我們本著學(xué)習(xí)的目的,來實(shí)現(xiàn)一個(gè)QQ空間圖片預(yù)覽Dialog。如果你偶然看過我上周的Blog,肯定知道,我在上周已經(jīng)寫了如何實(shí)現(xiàn)彈幕

教你寫一個(gè)彈幕庫,確定不了解一下?

所以我們可以直接在圖片預(yù)覽中拿來用就可以了。

最終效果
效果

如果你注意到細(xì)節(jié),發(fā)現(xiàn)這個(gè)庫還是很有趣的:

  • 彈幕
  • 眾多的手勢(很大一部分來自PhotoView
  • 隨著滑動(dòng)高度變化的背景透明度
  • 多種動(dòng)畫

由于之前我已經(jīng)講過如何實(shí)現(xiàn)彈幕,所以在本文中,不會(huì)涉及到如何實(shí)現(xiàn)彈幕,只會(huì)直接引用Muti-Barrage

目錄

目錄

一、整體把握

想要實(shí)現(xiàn)QQ空間的圖片預(yù)覽,我們可以使用什么?首先,我們的基礎(chǔ)肯定是一個(gè)Dialog;其次,圖片的切換可以使用ViewPager,同樣你也可以使用ViewPager2,可以支持縱向圖片切換和更好的切換動(dòng)畫過渡,不過,ViewPager2是屬于androidx的,如果使用ViewPager2,那么整個(gè)庫就需要遷移到androidx了;接著,手勢的處理及圖片我們可以采用PhotoView,至于彈幕我們可以采用之前寫好的Muti-Barrage;最后,你可能會(huì)問,使用了這么多第三方庫,我們還能大展身手嗎?剩下的工作就比較輕松了,主要負(fù)責(zé)觸摸事件和動(dòng)畫的處理。好了,現(xiàn)在整個(gè)結(jié)構(gòu)清晰了,ViewPager + PhotoView + Muti-BarrageView手勢處理+動(dòng)畫就可以構(gòu)成一個(gè)簡單的仿QQ空間的圖片預(yù)覽了。

1. 類圖

上面我們已經(jīng)知道需要使用什么技術(shù)去實(shí)現(xiàn)了,現(xiàn)在我們再看一下主要的UML類圖,從而方便我們下面的代碼實(shí)戰(zhàn)的講解:

UML類圖

聰明的你可能已經(jīng)發(fā)現(xiàn)了,這不是代理模式嗎?沒錯(cuò),如果你想對代理模式了解更多一點(diǎn),移步:

Android設(shè)計(jì)模式實(shí)戰(zhàn)-代理模式

對于一些瑣碎的類,UML類圖中并沒有給出。

二、代碼實(shí)戰(zhàn)

由于我們已經(jīng)上了UML類圖,那我們就按照UML類圖的順序講起吧。

1. IPhotoPager

public interface IPhotoPager {
    void show();
    void dismiss();
    void setConfig(Config config);

    /*
        config
     */
    class Config {
        List<String> paths;// 圖片路徑
        List<Bitmap> bitmaps; // Bitmap
        boolean canDelete = true; // 普通主題使用
        boolean isShowAnimation = false; // 是否展示動(dòng)畫
        boolean isShowBarrage = true; // 是否顯示彈幕
        int animationType; // 動(dòng)畫類型
        int startPosition = 0; // 圖片開始位置
        DeleteListener deleteListener; // 刪除監(jiān)聽器
        List<BarrageData> barrages; // 彈幕數(shù)據(jù)
    }
}

IPhotoPager定義一些基本的約束,以及我們需要使用的一些數(shù)據(jù)類型。

2. BasePager

public abstract class BasePager extends Dialog
        implements ViewPager.OnPageChangeListener,IPhotoPager {

    protected Context mContext;
    // all base info
    private IPhotoPager.Config mConfig;

    // basic info
    protected int curPosition;
    protected boolean isCanDelete;
    protected boolean isShowAnimation;
    protected int animationType;
    protected DeleteListener deleteListener;
    protected boolean isShowBarrages;

    protected List<Bitmap> bitmaps;
    protected List<BarrageData> barrages;

    public BasePager(@NonNull Context context) {
        this(context, R.style.Dialog);
    }

    public BasePager(@NonNull Context context, int themeResId) {
        super(context, themeResId);

        mContext = context;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Window window = getWindow();
        if (window != null) {
            window.setDimAmount(1f);
        }
    }

    //... 省略一些ViewPager的接口

    @Override
    public void setConfig(Config config) {
        this.mConfig = config;
        initParams();
    }

    /*
        init parameter
     */
    private void initParams() {
        this.isCanDelete = mConfig.canDelete;
        this.isShowAnimation = mConfig.isShowAnimation;
        this.animationType = mConfig.animationType;
        this.curPosition = mConfig.startPosition;

        // init bitmaps
        this.bitmaps = new ArrayList<>();
        this.bitmaps.addAll(mConfig.bitmaps);
        this.deleteListener = mConfig.deleteListener;
        this.barrages = mConfig.barrages;
        this.isShowBarrages = mConfig.isShowBarrage;
    }

    @Override
    public void show() {
        if(bitmaps == null || bitmaps.size() == 0){
            throw new RuntimeException("bitmaps can't be null");
        }

        super.show();

        // seting rect must be after dialog.showing(),otherwise dialog will show in initial size.
        Rect rect = new Rect();
        ((Activity) mContext).getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        // set position and size
        Window window = getWindow();
        WindowManager.LayoutParams lp = window.getAttributes();
        lp.gravity = Gravity.BOTTOM;
        lp.width = WindowManager.LayoutParams.MATCH_PARENT;
        lp.height = rect.height();
        window.setAttributes(lp);
        if (isShowAnimation) {
            if (animationType == ANIMATION_SCALE_ALPHA) {
                window.setWindowAnimations(R.style.PhotoPagerScale);
            } else if (animationType == ANIMATION_TRANSLATION) {
                window.setWindowAnimations(R.style.PhotoPagerTranslation);
            } else {
                // default animaiont is translation
                window.setWindowAnimations(R.style.PhotoPagerAlpha);
            }
        }
    }
}

BasePager內(nèi)容也挺簡單,實(shí)現(xiàn)ViewPager的監(jiān)聽器,雖然并不做什么內(nèi)容,其次就是將獲取到的Config對基礎(chǔ)的數(shù)據(jù)進(jìn)行初始化。

3. QQPager

QQPager的代碼將近400行左右,還是拆分按照過程講解。

3.1 數(shù)據(jù)初始化

數(shù)據(jù)初始化主要分為初始化ViewPagerMuti-BarrageView,簡單的初始化過程,這里就只是介紹我們的數(shù)據(jù)就好了:

public class QQPager extends BasePager {
    private static final String TAG = "QQPager";
    private static final int SCROLL_THRESHOlD = 100; // 滑動(dòng)的閾值
    private static final int MSG_UP = 0;

    private ImageView mBarrage; // 彈幕的開關(guān)
    private MyViewPager mPhotoPager; // 簡單處理過的ViewPager
    private TextView mPosition; // 位置信息
    private PhotoPagerAdapter mAdapter; // ViewPager的item就是PhotoView

    private BarrageView mBarrageView;
    private BarrageAdapter<BarrageData> mBarrageAdapter;
    private boolean isInitBarrage;

    private int touchSloop; // 滑動(dòng)的閾值
    private float lastX; // 上次事件的坐標(biāo)
    private float lastY;
    private float deltaY;
    private boolean isHorizontalMove = false; 
    private boolean isVerticalMove = false;
    private boolean isMove = false;
    private int clickCount = 0; // 判斷單擊還是雙擊,因?yàn)槿绻请p擊需要交給PhotoView處理

    private Handler mHandler = new QQPagerHandler(this);

    private static class QQPagerHandler extends Handler {
        private WeakReference<QQPager> mQQPagerReference;

        QQPagerHandler(QQPager qqPager) {
            this.mQQPagerReference = new WeakReference<QQPager>(qqPager);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            switch (msg.what) {
                case MSG_UP:
                    if (mQQPagerReference.get().clickCount == 1)
                        mQQPagerReference.get().dismiss();
                    else
                        mQQPagerReference.get().clickCount = 0;
                    break;
            }
        }
    }

    class TextViewHolder extends BarrageAdapter.BarrageViewHolder<BarrageData> {
        // ...代碼省略
    }

    class ViewHolder extends BarrageAdapter.BarrageViewHolder<BarrageData> {
        // ...代碼省略
    }
}

一些基礎(chǔ)的數(shù)據(jù)以及兩個(gè)類型的彈幕Holder,彈幕Holder的代碼被省略了,需要的可以看源碼。QQPagerHandler作用是判斷雙擊,具體的過程我們在下面講解。

3.2 事件分發(fā)

用過PhotoView的同學(xué)應(yīng)該都知道,雙擊是放大圖片,那么我們采用的既然是PhotoView,自然也是這樣的,以下是我們要在事件分發(fā)中考慮的地方:

  • 單擊關(guān)閉圖片預(yù)覽,我們需要阻止觸摸事件下發(fā),Dialog自身處理。
  • 雙擊需要交給ViewPager,再由ViewPager交給PhotoView處理。
  • 水平方向移動(dòng)就是ViewPager中圖片切換,事件交給ViewPager處理。
  • 豎直方向移動(dòng)就是移動(dòng)我們的ViewPager,Dialog自身處理,并且ViewPager縱向滑動(dòng)距離會(huì)影響背景的透明度。

說到這里,我想你應(yīng)該就明白了,只要處理單雙擊和縱橫向的判斷就好了,事實(shí)就是這么簡單,看代碼:

 public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
        if (isHorizontalMove)
            return super.dispatchTouchEvent(ev);

        float curX = ev.getX();// 獲取當(dāng)前坐標(biāo)
        float curY = ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPosition.setAlpha(1f); // Action_Down會(huì)觸發(fā)位置文本的顯示
                mPosition.setVisibility(View.VISIBLE);
                isMove = false;
                clickCount++; // 點(diǎn)擊次數(shù)增加
                break;
            case MotionEvent.ACTION_MOVE:
                float deltaX = curX - lastX;
                deltaY = curY - lastY;
                if (Math.abs(deltaX) > touchSloop || Math.abs(deltaY) > touchSloop) {
                    isMove = true;  // 滑動(dòng)距離大于閾值自動(dòng)重置點(diǎn)擊計(jì)數(shù)
                    clickCount = 0;
                }
                if (Math.abs(deltaX) < Math.abs(deltaY)) {
                    isVerticalMove = true; // 如果縱向距離大于橫向阻斷ViewPager事件下發(fā)
                    mPhotoPager.setIntercept(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (clickCount == 1 && !isMove &&
                        !isTouchPointInView(mBarrage,(int) ev.getRawX(),(int) ev.getRawY()))// 如果單擊的不是彈幕開關(guān)按鈕就發(fā)送消息
                    mHandler.sendEmptyMessageDelayed(MSG_UP, 400);
                else
                    clickCount = 0;
                break;
        }
        lastX = curX;
        lastY = curY;
        return super.dispatchTouchEvent(ev);
    }

    public boolean onTouchEvent(@NonNull MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mPhotoPager.scrollBy(0, (int) -deltaY);// ViewPager豎直移動(dòng)
                // set dialog's background alpha
                float offsetPercent = Math.abs(mPhotoPager.getScrollY() - 0f) / mPhotoPager.getMeasuredHeight();
                Log.e(TAG,"offset:"+offsetPercent);
                if (getWindow() != null)
                    getWindow().setDimAmount(1f - offsetPercent);
                break;

            case MotionEvent.ACTION_UP:
                if (isVerticalMove) {
                    if (Math.abs(mPhotoPager.getScrollY() - 0f) > SCROLL_THRESHOlD) {
                        scrollCloseAnimation();
                    } else {
                        rollbackAnimation();
                    }
                }
                break;
        }

        return super.onTouchEvent(event);
    }

很多東西代碼的注釋很詳細(xì)了,這邊我要補(bǔ)充一下:

  • 單雙擊是通過QQPagerHandler延遲發(fā)送400ms來判斷的,400ms內(nèi)單擊一次執(zhí)行關(guān)閉動(dòng)畫,如果再點(diǎn)擊一次就重置單擊計(jì)數(shù)。
  • QQPageronTouchEvent處理的時(shí)候,會(huì)通過getWindow().setDimAmount(1f - offsetPercent)改變背景的透明度。
  • 豎直方向移動(dòng)會(huì)阻斷ViewPager事件的下發(fā),所以,事件到最后還會(huì)交給自身處理,在手指釋放的時(shí)候,如果豎直方向移動(dòng)距離大于我們設(shè)置的最小滑動(dòng)閾值,就執(zhí)行滑動(dòng)關(guān)閉動(dòng)畫,否則,ViewPager會(huì)回滾,移動(dòng)到初始位置。

再來看一下手勢處理,雙擊、水平移動(dòng)、縱向移動(dòng):


演示
3.3 動(dòng)畫處理

圖片預(yù)覽需要用到兩種動(dòng)畫,View動(dòng)畫屬性動(dòng)畫,View動(dòng)畫在QQPager打開和關(guān)閉的時(shí)候使用,詳見上面的BasePagershow()方法,設(shè)置的style,這里不再介紹。屬性動(dòng)畫使用的場景就是位置文本定時(shí)顯示、ViewPager的回滾和滑動(dòng)退出,代碼類似,這里就挑滑動(dòng)退出講一下:

private void scrollCloseAnimation() {
        Window window = getWindow();
        if (window != null)
            window.setDimAmount(0f);
        if (deltaY > 0) {
            mPhotoPager.animate()
                    .y(mPhotoPager.getMeasuredHeight())
                    .setDuration(600)
                    .setListener(new SimpleAnimationListener() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            super.onAnimationEnd(animation);
                            //getWindow().setWindowAnimations(R.style.PhotoPagerAlpha);
                            dismiss();
                        }
                    })
                    .start();
        } else {
            mPhotoPager.animate()
                    .y(-mPhotoPager.getMeasuredHeight())
                    .setDuration(600)
                    .setListener(new SimpleAnimationListener() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            super.onAnimationEnd(animation);
                            //getWindow().setWindowAnimations(R.style.PhotoPagerAlpha);
                            dismiss();
                        }
                    })
                    .start();
        }
    }

不得不說,使用View本身的animate()來使用屬性動(dòng)畫還挺方便的,一次使用一次爽,次次使用次次爽~

4. PhotoPagerViewProxy

最后的最后,我們再來介紹以下代理類,主要用來構(gòu)建數(shù)據(jù):

public class PhotoPagerViewProxy implements IPhotoPager {
    public static final int TYPE_NORMAL = 1;
    public static final int TYPE_QQ = 2;
    public static final int TYPE_WE_CHAT = 3;

    public static final int ANIMATION_SCALE_ALPHA = 1;
    public static final int ANIMATION_TRANSLATION = 2;
    public static final int ANIMATION_ALPHA = 3;

    private BasePager photoPageView;

    private PhotoPagerViewProxy(Context context, int type, Config config) {
        switch (type) {
            case TYPE_QQ:
                photoPageView = new QQPager(context,R.style.Dialog);
                break;
            case TYPE_WE_CHAT:
                break;
            default:
                photoPageView = new NormalPager(context, R.style.Dialog);
                break;
        }
        setConfig(config);
    }

    @Override
    public void show() {
        photoPageView.show();
    }

    @Override
    public void dismiss() {
        photoPageView.dismiss();
    }

    @Override
    public void setConfig(Config config) {
        photoPageView.setConfig(config);
    }

    public static class Builder {
        private Activity context;
        private IPhotoPager.Config config;
        private int type;

        public Builder(Activity context, int type) {
            this.context = context;
            this.config = new IPhotoPager.Config();
            this.type = type;
        }

        public Builder(Activity context) {
            // default type is TYPE_NORMAL
            this(context, TYPE_NORMAL);
        }

        // ...同樣省略大段代碼,你只需要知道這里是初始化數(shù)據(jù)使用的Builder模式

        public PhotoPagerViewProxy create() {
            return new PhotoPagerViewProxy(context, type, config);
        }
    }
}

三、總結(jié)

總的來說,代碼量不大也不難,不過,這份代碼還有很多需要提高的地方,比如說,背景透明度隨著ViewPager的縱向滑動(dòng)距離的變化不是那么快等。當(dāng)然了,本人水平有限,難免有誤,如果你發(fā)現(xiàn)哪里有問題,歡迎指正~

Demo地址:PhotoPagerView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容