前言
弹幕除了做直播还能干什么?如果你看过QQ空间,就知道QQ空间的照片预览使用弹幕。今天,戴尔为了学习目的实施了QQ空间照片预览Dialog。如果你偶然看到我上周的博客,你肯定知道我上周已经知道如何实现弹幕了。
注意到细节,这个库还是很有趣的。
很多手势(大部分由PhotoView提供)滑动高度变化带来的背景透明度在各种动画之前已经说过了弹幕的实现方法,所以本文没有提及弹幕的实现方法,只是直接参考弹幕库。
目录
一个,整体把握
要实现QQ空间的照片预览,可以使用什么?第一,我们的基础肯定是Dialog是。第二,图片切换可以使用ViewPager。可以使用ViewPager2。可以支持纵向图片过渡和更好的过渡动画过渡,但ViewPager2属于androidx。ViewPager2要求将整个库迁移到androidx,然后可以使用手势处理和照片PhotoView。关于弹幕,以前编写的MUTI-BARRAGE;的规格化距离的幂函数。最后,使用了这么多第三方库,我们还能发挥实力吗?剩下的工作比较容易,主要负责触摸活动和动画处理。好了,现在整个结构清晰了,VIEW Pager Photoview Muti-Barrage VIEW和手势处理动画可以构成简单的仿QQ空间的照片预览。
“1 .类图“上面我们已经知道应该使用什么技术了。现在我们来看一下主要的UML类图。
聪明的你可能已经发现了。这不是代理模式吗?没错。
二、代码实战
我们已经有了UML类图,所以我们按照UML类图的顺序来谈吧。
1.IPhotoPager
PublicinterfaceIPhotoPager{
voidshow();
void dismiss();
Voidsetconfig(Configconfig):
/*
Config
*/
ClassConfig{
ListStringpaths//图片路径
ListBitmapbitmaps//位图
BooleancanDelete=true//使用一般主题
BooleanisShowAnimation=false//是否显示动画
BooleanisShowBarrage=true//是否显示弹幕
IntanimationType//动画类型
intstart position=0;//图形起始位置
DeleteListenerdeleteListener//删除监听器
ListBarrageDatabarrages//弹幕数据
}
}IPhotoPager定义了一些默认约束和要使用的数据类型。
“2 .主页面"
publicabstractclassbasepagerextendsdialog
implementsviewpager . onpagechangelistener、iphotopager
{ protected Context mContext; // all base info private IP 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.); } public BasePager(@NonNull Context context, int themeResId) { super(context, themeResId); mContext = context; } @Override protected void onCreate(Bundle savedInstanceState) { (savedInstanceState); window window = getWindow(); if (window != null) { window.setDimAmount(1f); } } //... 省略一些ViewPager的接口 @Override public void setConfig(Config config) { = config; initParams(); } /* init parameter */ private void initParams() { = mCon; = mCon; = mCon; = mCon; // init bitmaps = new ArrayList<>(); .addAll); = mCon; = mCon; = mCon; } @Override public void show() { if(bitmaps == null || bi() == 0){ throw new RuntimeException("bitmaps can't be null"); } (); // 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 = Gravi; lp.width = WindowManager.Layou; lp.height = rect.height(); window.setAttributes(lp); if (isShowAnimation) { if (animationType == ANIMATION_SCALE_ALPHA) { window.setWindowAnimation); } else if (animationType == ANIMATION_TRANSLATION) { window.setWindowAnimation); } else { // default animaiont is translation window.setWindowAnimation); } } } }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<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) {
= new WeakReference<QQPager>(qqPager);
}
@Override
public void handleMessage(Message msg) {
(msg);
switch ) {
case MSG_UP:
if ().clickCount == 1)
mQQPagerRe().dismiss();
else
mQQPagerRe().clickCount = 0;
break;
}
}
}
class TextViewHolder extends BarrageAda;BarrageData> {
// ...代码省略
}
class ViewHolder extends BarrageAda;BarrageData> {
// ...代码省略
}
}
一些基础的数据以及两个类型的弹幕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 (ev);
float curX = ev.getX();// 获取当前坐标
float curY = ev.getY();
switch ()) {
case Mo:
mPo(1f); // Action_Down会触发位置文本的显示
mPo);
isMove = false;
clickCount++; // 点击次数增加
break;
case Mo:
float deltaX = curX - lastX;
deltaY = curY - lastY;
if (deltaX) > touchSloop || Ma(deltaY) > touchSloop) {
isMove = true; // 滑动距离大于阈值自动重置点击计数
clickCount = 0;
}
if (deltaX) < Ma(deltaY)) {
isVerticalMove = true; // 如果纵向距离大于横向阻断ViewPager事件下发
mP(true);
}
break;
case Mo:
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 (ev);
}
public boolean onTouchEvent(@NonNull MotionEvent event) {
switch ()) {
case Mo:
mP(0, (int) -deltaY);// ViewPager竖直移动
// set dialog background alpha
float offsetPercent = Ma() - 0f) / mP();
Log.e(TAG,"offset:"+offsetPercent);
if (getWindow() != null)
getWindow().setDimAmount(1f - offsetPercent);
break;
case Mo:
if (isVerticalMove) {
if () - 0f) > SCROLL_THRESHOlD) {
scrollCloseAnimation();
} else {
rollbackAnimation();
}
}
break;
}
return (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) {
mP()
.y(mP())
.setDuration(600)
.setListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
(animation);
//getWindow().setWindowAnimation);
dismiss();
}
})
.start();
} else {
mP()
.y(-mP())
.setDuration(600)
.setListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
(animation);
//getWindow().setWindowAnimation);
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.);
break;
case TYPE_WE_CHAT:
break;
default:
photoPageView = new NormalPager(context, R.);
break;
}
setConfig(config);
}
@Override
public void show() {
();
}
@Override
public void dismiss() {
();
}
@Override
public void setConfig(Config config) {
(config);
}
public static class Builder {
private Activity context;
private IP config;
private int type;
public Builder(Activity context, int type) {
= context;
= new IP();
= 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地址:
Android核心知识点笔记github: