天天看點

一個圖檔 在另一個圖檔定位_仿寫一個QQ空間圖檔預覽Dialog前言目錄一、整體把握二、代碼實戰三、總結

前言

彈幕除了能用來做直播,還能用來做什麼?如果你看過QQ空間,你肯定知道,QQ空間的圖檔預覽使用了彈幕。今天,我們本着學習的目的,來實作一個QQ空間圖檔預覽Dialog。如果你偶然看過我上周的Blog,肯定知道,我在上周已經寫了如何實作彈幕 https://www.jianshu.com/p/2b1f4da434f3

一個圖檔 在另一個圖檔定位_仿寫一個QQ空間圖檔預覽Dialog前言目錄一、整體把握二、代碼實戰三、總結

如果你注意到細節,發現這個庫還是很有趣的:

  • 彈幕
  • 衆多的手勢(很大一部分來自PhotoView)
  • 随着滑動高度變化的背景透明度
  • 多種動畫
  • 由于之前我已經講過如何實作彈幕,是以在本文中,不會涉及到如何實作彈幕,隻會直接引用

彈幕庫:https://github.com/mCyp/Muti-Barrage

目錄

一個圖檔 在另一個圖檔定位_仿寫一個QQ空間圖檔預覽Dialog前言目錄一、整體把握二、代碼實戰三、總結

一、整體把握

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

「1. 類圖」上面我們已經知道需要使用什麼技術去實作了,現在我們再看一下主要的UML類圖,進而友善我們下面的代碼實戰的講解:

一個圖檔 在另一個圖檔定位_仿寫一個QQ空間圖檔預覽Dialog前言目錄一、整體把握二、代碼實戰三、總結

聰明的你可能已經發現了,這不是代理模式嗎?沒錯

二、代碼實戰

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

「1. IPhotoPager」

public interface IPhotoPager {    void show();    void dismiss();    void setConfig(Config config);    /*        config     */    class Config {        List paths;// 圖檔路徑        List bitmaps; // Bitmap        boolean canDelete = true; // 普通主題使用        boolean isShowAnimation = false; // 是否展示動畫        boolean isShowBarrage = true; // 是否顯示彈幕        int animationType; // 動畫類型        int startPosition = 0; // 圖檔開始位置        DeleteListener deleteListener; // 删除監聽器        List 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 bitmaps;    protected List 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 資料初始化

資料初始化主要分為初始化ViewPager和Muti-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 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 mQQPagerReference;        QQPagerHandler(QQPager qqPager) {            this.mQQPagerReference = new WeakReference(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 {        // ...代碼省略    }    class ViewHolder extends BarrageAdapter.BarrageViewHolder {        // ...代碼省略    }}
           

一些基礎的資料以及兩個類型的彈幕Holder,彈幕Holder的代碼被省略了,需要的可以看源碼。QQPagerHandler作用是判斷輕按兩下,具體的過程我們在下面講解。

3.2 事件分發

用過PhotoView的同學應該都知道,輕按兩下是放大圖檔,那麼我們采用的既然是PhotoView,自然也是這樣的,以下是我們要在事件分發中考慮的地方:

單擊關閉圖檔預覽,我們需要阻止觸摸事件下發,Dialog自身處理。輕按兩下需要交給ViewPager,再由ViewPager交給PhotoView處理。水準方向移動就是ViewPager中圖檔切換,事件交給ViewPager處理。豎直方向移動就是移動我們的ViewPager,Dialog自身處理,并且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)  SCROLL_THRESHOlD) {                        scrollCloseAnimation();                    } else {                        rollbackAnimation();                    }                }                break;        }        return super.onTouchEvent(event);    }
           

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

  • 單輕按兩下是通過QQPagerHandler延遲發送400ms來判斷的,400ms内單擊一次執行關閉動畫,如果再點選一次就重置單擊計數。
  • QQPager在onTouchEvent處理的時候,會通過getWindow().setDimAmount(1f - offsetPercent)改變背景的透明度。
  • 豎直方向移動會阻斷ViewPager事件的下發,是以,事件到最後還會交給自身處理,在手指釋放的時候,如果豎直方向移動距離大于我們設定的最小滑動門檻值,就執行滑動關閉動畫,否則,ViewPager會復原,移動到初始位置。

再來看一下手勢處理,輕按兩下、水準移動、縱向移動:

一個圖檔 在另一個圖檔定位_仿寫一個QQ空間圖檔預覽Dialog前言目錄一、整體把握二、代碼實戰三、總結

3.3 動畫處理

圖檔預覽需要用到兩種動畫,View動畫和屬性動畫,View動畫在QQPager打開和關閉的時候使用,詳見上面的BasePager的show()方法,設定的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);        }    }}
           

三、總結

總的來說,代碼量不大也不難,不過,這份代碼還有很多需要提高的地方,比如說,背景透明度随着ViewPager的縱向滑動距離的變化不是那麼快等。當然了,本人水準有限,難免有誤,如果你發現哪裡有問題,歡迎指正

Over~ Demo位址:https://github.com/mCyp/PhotoPagerView

Android核心知識點筆記github:https://github.com/AndroidCot/Android