2020-07-30 18:20:56

仿写一个QQ空间图片预览Dialog

20 / 0 / 0 / 0

前言

弹幕除了能用来做直播,还能用来做什么?如果你看过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<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);  
        }  
    }  
}  

三、总结

总的来说,代码量不大也不难,不过,这份代码还有很多需要提高的地方,比如说,背景透明度随着ViewPager的纵向滑动距离的变化不是那么快等。当然了,本人水平有限,难免有误,如果你发现哪里有问题,欢迎指正

Over~ Demo地址:https://github.com/mCyp/PhotoPagerView

PS: 如本文对您有疑惑,可加QQ:1752338621 进行讨论。

0 条评论

0
0
官方
微信
官方微信
Q Q
咨询
意见
反馈
返回
顶部