前言
彈幕除了能用來做直播,還能用來做什麼?如果你看過QQ空間,你肯定知道,QQ空間的圖檔預覽使用了彈幕。今天,我們本着學習的目的,來實作一個QQ空間圖檔預覽Dialog。如果你偶然看過我上周的Blog,肯定知道,我在上周已經寫了如何實作彈幕 https://www.jianshu.com/p/2b1f4da434f3
如果你注意到細節,發現這個庫還是很有趣的:
- 彈幕
- 衆多的手勢(很大一部分來自PhotoView)
- 随着滑動高度變化的背景透明度
- 多種動畫
- 由于之前我已經講過如何實作彈幕,是以在本文中,不會涉及到如何實作彈幕,隻會直接引用
彈幕庫:https://github.com/mCyp/Muti-Barrage
目錄
一、整體把握
想要實作QQ空間的圖檔預覽,我們可以使用什麼?首先,我們的基礎肯定是一個Dialog;其次,圖檔的切換可以使用ViewPager,同樣你也可以使用ViewPager2,可以支援縱向圖檔切換和更好的切換動畫過渡,不過,ViewPager2是屬于androidx的,如果使用ViewPager2,那麼整個庫就需要遷移到androidx了;接着,手勢的處理及圖檔我們可以采用PhotoView,至于彈幕我們可以采用之前寫好的Muti-Barrage;最後,你可能會問,使用了這麼多第三方庫,我們還能大展身手嗎?剩下的工作就比較輕松了,主要負責觸摸事件和動畫的處理。好了,現在整個結構清晰了,ViewPager + PhotoView + Muti-BarrageView和手勢處理+動畫就可以構成一個簡單的仿QQ空間的圖檔預覽了。
「1. 類圖」上面我們已經知道需要使用什麼技術去實作了,現在我們再看一下主要的UML類圖,進而友善我們下面的代碼實戰的講解:
聰明的你可能已經發現了,這不是代理模式嗎?沒錯
二、代碼實戰
由于我們已經上了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會復原,移動到初始位置。
再來看一下手勢處理,輕按兩下、水準移動、縱向移動:
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