仿寫一個QQ空間圖片預覽Dialog

前言

彈幕除了能用來作直播,還能用來作什麼?若是你看過QQ空間,你確定知道,QQ空間的圖片預覽使用了彈幕。今天,咱們本着學習的目的,來實現一個QQ空間圖片預覽Dialog。若是你偶然看過我上週的Blog,確定知道,我在上週已經寫了如何實現彈幕android

教你寫一個彈幕庫,肯定不瞭解一下?git

因此咱們能夠直接在圖片預覽中拿來用就能夠了。github

最終效果

效果

若是你注意到細節,發現這個庫仍是頗有趣的:設計模式

  • 彈幕
  • 衆多的手勢(很大一部分來自PhotoView
  • 隨着滑動高度變化的背景透明度
  • 多種動畫

因爲以前我已經講過如何實現彈幕,因此在本文中,不會涉及到如何實現彈幕,只會直接引用Muti-Barragebash

目錄

目錄

1、總體把握

想要實現QQ空間的圖片預覽,咱們可使用什麼?首先,咱們的基礎確定是一個Dialog;其次,圖片的切換可使用ViewPager,一樣你也可使用ViewPager2,能夠支持縱向圖片切換和更好的切換動畫過渡,不過,ViewPager2是屬於androidx的,若是使用ViewPager2,那麼整個庫就須要遷移到androidx了;接着,手勢的處理及圖片咱們能夠採用PhotoView,至於彈幕咱們能夠採用以前寫好的Muti-Barrage;最後,你可能會問,使用了這麼多第三方庫,咱們還能大展身手嗎?剩下的工做就比較輕鬆了,主要負責觸摸事件和動畫的處理。好了,如今整個結構清晰了,ViewPager + PhotoView + Muti-BarrageView手勢處理+動畫就能夠構成一個簡單的仿QQ空間的圖片預覽了。ide

1. 類圖

上面咱們已經知道須要使用什麼技術去實現了,如今咱們再看一下主要的UML類圖,從而方便咱們下面的代碼實戰的講解: oop

UML類圖
聰明的你可能已經發現了,這不是 代理模式嗎?沒錯,若是你想對 代理模式瞭解更多一點,移步:

Android設計模式實戰-代理模式學習

對於一些瑣碎的類,UML類圖中並無給出。動畫

2、代碼實戰

因爲咱們已經上了UML類圖,那咱們就按照UML類圖的順序講起吧。ui

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; // 是否展現動畫
        boolean isShowBarrage = true; // 是否顯示彈幕
        int animationType; // 動畫類型
        int startPosition = 0; // 圖片開始位置
        DeleteListener deleteListener; // 刪除監聽器
        List<BarrageData> barrages; // 彈幕數據
    }
}
複製代碼

IPhotoPager定義一些基本的約束,以及咱們須要使用的一些數據類型。

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內容也挺簡單,實現ViewPager的監聽器,雖然並不作什麼內容,其次就是將獲取到的Config對基礎的數據進行初始化。

3. QQPager

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

3.1 數據初始化

數據初始化主要分爲初始化ViewPagerMuti-BarrageView,簡單的初始化過程,這裏就只是介紹咱們的數據就行了:

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

    private ImageView mBarrage; // 彈幕的開關
    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; // 滑動的閾值
    private float lastX; // 上次事件的座標
    private float lastY;
    private float deltaY;
    private boolean isHorizontalMove = false; 
    private boolean isVerticalMove = false;
    private boolean isMove = false;
    private int clickCount = 0; // 判斷單擊仍是雙擊,由於若是是雙擊須要交給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> {
        // ...代碼省略
    }
}
複製代碼

一些基礎的數據以及兩個類型的彈幕Holder,彈幕Holder的代碼被省略了,須要的能夠看源碼。QQPagerHandler做用是判斷雙擊,具體的過程咱們在下面講解。

3.2 事件分發

用過PhotoView的同窗應該都知道,雙擊是放大圖片,那麼咱們採用的既然是PhotoView,天然也是這樣的,如下是咱們要在事件分發中考慮的地方:

  • 單擊關閉圖片預覽,咱們須要阻止觸摸事件下發,Dialog自身處理。
  • 雙擊須要交給ViewPager,再由ViewPager交給PhotoView處理。
  • 水平方向移動就是ViewPager中圖片切換,事件交給ViewPager處理。
  • 豎直方向移動就是移動咱們的ViewPagerDialog自身處理,而且ViewPager縱向滑動距離會影響背景的透明度。

說到這裏,我想你應該就明白了,只要處理單雙擊和縱橫向的判斷就行了,事實就是這麼簡單,看代碼:

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

        float curX = ev.getX();// 獲取當前座標
        float curY = ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPosition.setAlpha(1f); // Action_Down會觸發位置文本的顯示
                mPosition.setVisibility(View.VISIBLE);
                isMove = false;
                clickCount++; // 點擊次數增長
                break;
            case MotionEvent.ACTION_MOVE:
                float deltaX = curX - lastX;
                deltaY = curY - lastY;
                if (Math.abs(deltaX) > touchSloop || Math.abs(deltaY) > touchSloop) {
                    isMove = true;  // 滑動距離大於閾值自動重置點擊計數
                    clickCount = 0;
                }
                if (Math.abs(deltaX) < Math.abs(deltaY)) {
                    isVerticalMove = true; // 若是縱向距離大於橫向阻斷ViewPager事件下發
                    mPhotoPager.setIntercept(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (clickCount == 1 && !isMove &&
                        !isTouchPointInView(mBarrage,(int) ev.getRawX(),(int) ev.getRawY()))// 若是單擊的不是彈幕開關按鈕就發送消息
                    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豎直移動
                // set dialog 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);
    }

複製代碼

不少東西代碼的註釋很詳細了,這邊我要補充一下:

  • 單雙擊是經過QQPagerHandler延遲發送400ms來判斷的,400ms內單擊一次執行關閉動畫,若是再點擊一次就重置單擊計數。
  • QQPageronTouchEvent處理的時候,會經過getWindow().setDimAmount(1f - offsetPercent)改變背景的透明度。
  • 豎直方向移動會阻斷ViewPager事件的下發,因此,事件到最後還會交給自身處理,在手指釋放的時候,若是豎直方向移動距離大於咱們設置的最小滑動閾值,就執行滑動關閉動畫,不然,ViewPager會回滾,移動到初始位置。

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

演示

3.3 動畫處理

圖片預覽須要用到兩種動畫,View動畫屬性動畫,View動畫在QQPager打開和關閉的時候使用,詳見上面的BasePagershow()方法,設置的style,這裏再也不介紹。屬性動畫使用的場景就是位置文本定時顯示、ViewPager的回滾和滑動退出,代碼相似,這裏就挑滑動退出講一下:

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()來使用屬性動畫還挺方便的,一次使用一次爽,次次使用次次爽~

4. PhotoPagerViewProxy

最後的最後,咱們再來介紹如下代理類,主要用來構建數據:

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);
        }

        // ...一樣省略大段代碼,你只須要知道這裏是初始化數據,使用的Builder模式

        public PhotoPagerViewProxy create() {
            return new PhotoPagerViewProxy(context, type, config);
        }
    }
}
複製代碼

3、總結

總的來講,代碼量不大也不難,不過,這份代碼還有不少須要提升的地方,好比說,背景透明度隨着ViewPager的縱向滑動距離的變化不是那麼快等。固然了,本人水平有限,不免有誤,若是你發現哪裏有問題,歡迎指正

Over~

Demo地址:PhotoPagerView

相關文章
相關標籤/搜索