QQ空间说说都有弹幕咯

来自:TeaOf

0
前言


弹幕除了能用来做直播,还能用来做什么?如果你看过QQ空间,你肯定知道,QQ空间的图片预览使用了弹幕。今天,我们本着学习的目的,来实现一个QQ空间图片预览Dialog。


我之前已经写了如何实现弹幕:


教你写一个弹幕库,确定不了解一下?


所以我们可以直接在图片预览中拿来用就可以了。


最终效果


如果你注意到细节,发现这个库还是很有趣的:


  • 弹幕

  • 众多的手势(很大一部分来自PhotoView)

  • 随着滑动高度变化的背景透明度

  • 多种动画


由于之前我已经讲过如何实现弹幕,所以在本文中,不会涉及到如何实现弹幕,只会直接引用Muti-Barrage。


目录



1
整体把握


想要实现QQ空间的图片预览,我们可以使用什么?


首先,我们的基础肯定是一个Dialog;其次,图片的切换可以使用ViewPager,同样你也可以使用ViewPager2,可以支持纵向图片切换和更好的切换动画过渡,不过,ViewPager2是属于androidx的,如果使用ViewPager2,那么整个库就需要迁移到androidx了;接着,手势的处理及图片我们可以采用PhotoView,至于弹幕我们可以采用之前写好的Muti-Barrage;


最后,你可能会问,使用了这么多第三方库,我们还能大展身手吗?


剩下的工作就比较轻松了,主要负责触摸事件和动画的处理。


好了,现在整个结构清晰了,ViewPager + PhotoView + Muti-BarrageView和手势处理+动画就可以构成一个简单的仿QQ空间的图片预览了。


上面我们已经知道需要使用什么技术去实现了,现在我们再看一下主要的UML类图,从而方便我们下面的代码实战的讲解:



聪明的你可能已经发现了,这不是代理模式吗?没错,如果你想对代理模式了解更多一点,移步:


Android设计模式实战-代理模式

https://www.jianshu.com/p/812be8af10c0


对于一些琐碎的类,UML类图中并没有给出。


2
?代码实战


由于我们已经上了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<BarrageData>?{
????????//?...代码省略
????}

????class?ViewHolder?extends?BarrageAdapter.BarrageViewHolder<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?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)?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's?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会回滚,移动到初始位置。


再来看一下手势处理,双击、水平移动、纵向移动:


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的纵向滑动距离的变化不是那么快等。当然了,本人水平有限,难免有误,如果你发现哪里有问题,欢迎指正~


Demo地址:

https://github.com/mCyp/PhotoPagerView

推荐↓↓↓
安卓开发
上一篇:这交互炸了系列 第十四式 之 百步穿扬