彈幕除了能用來作直播,還能用來作什麼?若是你看過QQ空間,你確定知道,QQ空間的圖片預覽使用了彈幕。今天,咱們本着學習的目的,來實現一個QQ空間圖片預覽Dialog。若是你偶然看過我上週的Blog,確定知道,我在上週已經寫了如何實現彈幕android
因此咱們能夠直接在圖片預覽中拿來用就能夠了。github
若是你注意到細節,發現這個庫仍是頗有趣的:設計模式
PhotoView
)因爲以前我已經講過如何實現彈幕,因此在本文中,不會涉及到如何實現彈幕,只會直接引用Muti-Barragebash
想要實現QQ空間的圖片預覽,咱們可使用什麼?首先,咱們的基礎確定是一個Dialog
;其次,圖片的切換可使用ViewPager
,一樣你也可使用ViewPager2
,能夠支持縱向圖片切換和更好的切換動畫過渡,不過,ViewPager2
是屬於androidx
的,若是使用ViewPager2
,那麼整個庫就須要遷移到androidx
了;接着,手勢的處理及圖片咱們能夠採用PhotoView,至於彈幕咱們能夠採用以前寫好的Muti-Barrage
;最後,你可能會問,使用了這麼多第三方庫,咱們還能大展身手嗎?剩下的工做就比較輕鬆了,主要負責觸摸事件和動畫的處理。好了,如今整個結構清晰了,ViewPager + PhotoView + Muti-BarrageView
和手勢處理+動畫
就能夠構成一個簡單的仿QQ空間的圖片預覽了。ide
上面咱們已經知道須要使用什麼技術去實現了,如今咱們再看一下主要的UML類圖,從而方便咱們下面的代碼實戰的講解: oop
代理模式
嗎?沒錯,若是你想對
代理模式
瞭解更多一點,移步:
對於一些瑣碎的類,UML類圖中並無給出。動畫
因爲咱們已經上了UML類圖,那咱們就按照UML類圖的順序講起吧。ui
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
定義一些基本的約束,以及咱們須要使用的一些數據類型。
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
對基礎的數據進行初始化。
QQPager
的代碼將近400行左右,仍是拆分按照過程講解。
數據初始化主要分爲初始化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<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
做用是判斷雙擊,具體的過程咱們在下面講解。
用過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) < 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
內單擊一次執行關閉動畫,若是再點擊一次就重置單擊計數。QQPager
在onTouchEvent
處理的時候,會經過getWindow().setDimAmount(1f - offsetPercent)
改變背景的透明度。ViewPager
事件的下發,因此,事件到最後還會交給自身處理,在手指釋放的時候,若是豎直方向移動距離大於咱們設置的最小滑動閾值,就執行滑動關閉動畫,不然,ViewPager
會回滾,移動到初始位置。再來看一下手勢處理,雙擊、水平移動、縱向移動:
圖片預覽須要用到兩種動畫,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()
來使用屬性動畫還挺方便的,一次使用一次爽,次次使用次次爽~
最後的最後,咱們再來介紹如下代理類,主要用來構建數據:
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地址:PhotoPagerView