内容简介:弹幕除了能用来做直播,还能用来做什么?如果你看过QQ空间,你肯定知道,QQ空间的图片预览使用了弹幕。今天,我们本着学习的目的,来实现一个QQ空间图片预览Dialog。如果你偶然看过我上周的Blog,肯定知道,我在上周已经写了如何实现弹幕所以我们可以直接在图片预览中拿来用就可以了。
弹幕除了能用来做直播,还能用来做什么?如果你看过QQ空间,你肯定知道,QQ空间的图片预览使用了弹幕。今天,我们本着学习的目的,来实现一个QQ空间图片预览Dialog。如果你偶然看过我上周的Blog,肯定知道,我在上周已经写了如何实现弹幕
所以我们可以直接在图片预览中拿来用就可以了。
最终效果
如果你注意到细节,发现这个库还是很有趣的:
PhotoView
由于之前我已经讲过如何实现弹幕,所以在本文中,不会涉及到如何实现弹幕,只会直接引用 Muti-Barrage
目录
一、整体把握
想要实现QQ空间的图片预览,我们可以使用什么?首先,我们的基础肯定是一个 Dialog
;其次,图片的切换可以使用 ViewPager
,同样你也可以使用 ViewPager2
,可以支持纵向图片切换和更好的切换动画过渡,不过, ViewPager2
是属于 androidx
的,如果使用 ViewPager2
,那么整个库就需要迁移到 androidx
了;接着,手势的处理及图片我们可以采用 PhotoView ,至于弹幕我们可以采用之前写好的 Muti-Barrage
;最后,你可能会问,使用了这么多第三方库,我们还能大展身手吗?剩下的工作就比较轻松了,主要负责触摸事件和动画的处理。好了,现在整个结构清晰了, ViewPager + PhotoView + Muti-BarrageView
和 手势处理+动画
就可以构成一个简单的仿QQ空间的图片预览了。
1. 类图
上面我们已经知道需要使用什么技术去实现了,现在我们再看一下主要的UML类图,从而方便我们下面的代码实战的讲解:
聪明的你可能已经发现了,这不是代理模式
吗?没错,如果你想对
代理模式
了解更多一点,移步:
对于一些琐碎的类,UML类图中并没有给出。
二、代码实战
由于我们已经上了UML类图,那我们就按照UML类图的顺序讲起吧。
1. IPhotoPager
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
定义一些基本的约束,以及我们需要使用的一些数据类型。
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<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
对基础的数据进行初始化。
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) { 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
作用是判断双击,具体的过程我们在下面讲解。
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) < 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
会回滚,移动到初始位置。
再来看一下手势处理,双击、水平移动、纵向移动:
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); } } } 复制代码
以上所述就是小编给大家介绍的《仿写一个QQ空间图片预览Dialog》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 少侠,留步,图片预览术
- JS图片压缩预览/下载
- 附件在线预览组件 WDA 1.2.6 发布,支持图片预览功能
- 造轮子之图片预览组件(preview)
- php使用jquery Form 实现页面无刷新上传图片,并预览图片
- HeyUI 1.5.0 版本更新,新增图片预览、抽屉组件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。