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

栏目: IOS · Android · 发布时间: 5年前

内容简介:弹幕除了能用来做直播,还能用来做什么?如果你看过QQ空间,你肯定知道,QQ空间的图片预览使用了弹幕。今天,我们本着学习的目的,来实现一个QQ空间图片预览Dialog。如果你偶然看过我上周的Blog,肯定知道,我在上周已经写了如何实现弹幕所以我们可以直接在图片预览中拿来用就可以了。

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

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

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

最终效果

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

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

PhotoView

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

目录

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

一、整体把握

想要实现QQ空间的图片预览,我们可以使用什么?首先,我们的基础肯定是一个 Dialog ;其次,图片的切换可以使用 ViewPager ,同样你也可以使用 ViewPager2 ,可以支持纵向图片切换和更好的切换动画过渡,不过, ViewPager2 是属于 androidx 的,如果使用 ViewPager2 ,那么整个库就需要迁移到 androidx 了;接着,手势的处理及图片我们可以采用 PhotoView ,至于弹幕我们可以采用之前写好的 Muti-Barrage ;最后,你可能会问,使用了这么多第三方库,我们还能大展身手吗?剩下的工作就比较轻松了,主要负责触摸事件和动画的处理。好了,现在整个结构清晰了, ViewPager + PhotoView + Muti-BarrageView手势处理+动画 就可以构成一个简单的仿QQ空间的图片预览了。

1. 类图

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

仿写一个QQ空间图片预览Dialog
聪明的你可能已经发现了,这不是 代理模式 吗?没错,如果你想对 代理模式

了解更多一点,移步:

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

对于一些琐碎的类,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 数据初始化

数据初始化主要分为初始化 ViewPagerMuti-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 处理。
  • 竖直方向移动就是移动我们的 ViewPagerDialog 自身处理,并且 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 内单击一次执行关闭动画,如果再点击一次就重置单击计数。
  • QQPageronTouchEvent 处理的时候,会通过 getWindow().setDimAmount(1f - offsetPercent) 改变背景的透明度。
  • 竖直方向移动会阻断 ViewPager 事件的下发,所以,事件到最后还会交给自身处理,在手指释放的时候,如果竖直方向移动距离大于我们设置的最小滑动阈值,就执行滑动关闭动画,否则, ViewPager 会回滚,移动到初始位置。

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

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

3.3 动画处理

图片预览需要用到两种动画, View动画属性动画 ,View动画在 QQPager 打开和关闭的时候使用,详见上面的 BasePagershow() 方法,设置的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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品故事地图

产品故事地图

唐娜·理查(Donna Lichaw) / 向振东 / 机械工业出版社 / 2017-6 / 49.9元

本书一共8章,分为三个部分:第1-2章讲述故事的作用、你该如何运用产品故事来吸引顾客,不是通过讲故事,而是创造故事。第3-5章阐述了不同情境和客户生命周期中的产品故事类型。第6-8章进一步研究如何在战略和策略层面发现、提升、用好你的产品故事。 《产品故事地图》写给那些想要通过创造出顾客喜欢用、经常用而且会推荐给别人用的产品来吸引客户的人。这里的“产品”包括网页、软件、APP、数字化或非数字化......一起来看看 《产品故事地图》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具