防微信多图选择器 (基础功能)

本文 主要写一个本人没事的时候写的一款防微信多图选择器 实现了包括微信的样式 剪切 涂鸦能功能

  代码纯原生实现,将会介绍基础的图片压缩  基础的自定义view的处理方法 以及一些动画和矩阵处理相关

1. 系统图库的筛选

首先 初始化一个线程

 mService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

读取图库 并且按照日期 从近到远排序

 Cursor cursor = getContentResolver().
                    query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA,
                                    MediaStore.Images.Media.DATE_ADDED},
                            null, null,
                            MediaStore.Images.Media.DATE_ADDED + " desc");
 mAllDirs.add(new Folder("", null, null));
            while (cursor.moveToNext()) {
                int date = cursor.getInt(1);
                String path = cursor.getString(0);
                if (!Utils.isPicture(path)) {
                    continue;
                }
                ImageInfo info = new ImageInfo(date, path);
                mAllPictures.add(info);

                String dir = new File(path).getParentFile().getAbsolutePath();
                Folder f = getFolder(dir);
                if (f == null) {
                    List<ImageInfo> folderImage = new ArrayList<>();
                    folderImage.add(info);
                    Folder folder = new Folder(dir, info, folderImage);
                    mAllDirs.add(folder);
                } else {
                    f.getImageInfos().add(info);
                }
            }
            cursor.close();

将得到的路径 保存 并且获取其父目录 保存为目录(后续需要按照目录显示 ) 即 一个目录对应多个图片,因为需要一个所有目录 我们将在数组中添加一个空目录

2.将得到的图片压缩

按照微信 每行显示4张图片 ,这是我们的目标尺寸,因为压缩比较耗时 ,我们需要使用刚才创建的线程池。为此 我们创一个任务 用于显示图片的压缩 (下次任务 后续 也将用到),gif 图 使用movie 实现

  @Override
    public void run() {
        Bitmap bitmap = mCache.get(mPath);
        boolean isSize = false;
        if (bitmap != null) {
            int widthDif = bitmap.getWidth() / mWidth;
            int heightDif = bitmap.getHeight() / mHeight;
            //如果 宽高的相差在0.5倍 到2倍之间  我们就认为 不需要再次压缩
            isSize = widthDif > 0.5f && widthDif < 2f || heightDif > 0.5f && heightDif < 2f;
        }
        if (bitmap == null || !isSize) {
            bitmap = Utils.compress(mPath, mWidth, mHeight);
            mCache.put(mPath, bitmap);
        }
        final Bitmap finalBitmap = bitmap;
        mHost.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTarget.setImageBitmap(finalBitmap);
                mTarget.setScaleX(1);
            }
        });
    }

关于压缩方法 此处为根据目标图片的宽高进行采样

 BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);
        int sample = Math.max(options.outWidth / width, options.outHeight / height);
        options.inSampleSize = sample;
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(path, options);

至此 所有的 图片已经显示在列表中
这里写图片描述

3.目录的选择与隐藏

在初始化图片的时候 已经将目录列表初始化 并且 隐藏 
  mDirList.setLayoutManager(new LinearLayoutManager(MultiPicturesSelectorActivity.this));
                mDirList.setAdapter(new DirsAdapter());
                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mDirList.getLayoutParams();
                int mTranslateSize = mContainer.getHeight() * 7 / 8;
                params.height = mTranslateSize;
                mDirList.setLayoutParams(params);
                mDirList.setTranslationY(mTranslateSize);

如上 将目录高度出示为 屏幕高度的7/8 然后将其向下偏移 来达到隐藏的效果 当用户点击查看目录 的时候 将其显示 或者隐藏

 private void animEnter() {
        mShadow.setVisibility(View.VISIBLE);
        mDirList.animate().translationYBy(-mDirList.getHeight()).setDuration(300).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        }).start();
    }

    private void animHide() {
        mDirList.animate().translationYBy(mDirList.getHeight()).setDuration(300).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mShadow.setVisibility(View.GONE);
            }
        }).start();
    }

上述中 阴影 为 遮盖下面的图片的一个半透明view
这里写图片描述

4 图片的预览

点击图片将会跳转到预览界面 首先拿到路径 并且将其放入viewpage中 开始进入默认全屏 当用户单击图片 显示状态栏

    int statusBarHeight = -1;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根据资源ID获取响应的尺寸值
            statusBarHeight = getResources().getDimensionPixelSize(resourceId);
        }
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mPreviewVp.getLayoutParams();
        if (isShow)
            params.topMargin = -statusBarHeight;
        else
            params.topMargin = 0;
        mPreviewVp.setLayoutParams(params);

解释下 需要实现一个-的margin 否则 进入全屏状态 将会因为 上边距 而出现 左右的间隙

 private class HandleSingleTap implements ScaleImageView.OnGestureListener {
        @Override
        public void onSingleTapUp() {
            if (isShow) {
                isShow = false;
                getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
                mTopBar.animate().translationYBy(-mTopBar.getHeight()).setDuration(200).start();
                //mBottomBar.animate().alpha(0).setDuration(200).start();
                mBottomBar.setAlpha(0);
                mBottomBar.setVisibility(View.GONE);
            } else {
                isShow = true;
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
                mTopBar.animate().translationYBy(mTopBar.getHeight()).setDuration(200).start();
                mBottomBar.setVisibility(View.VISIBLE);
                mBottomBar.animate().alpha(0.8f).setDuration(200).start();
            }
            initTop();
        }
    }

同时控制下边距的透明状态 此单击事件 从自定义的imageView 传入

5.缩放ImageView

1)实现基础缩放功能

基础配置请参考代码 哈 关于 缩放 我们主要介绍的是手势实现
首先初始化图片的初始位置 (需要在onMeasure中获取宽高)

 private void init() {
        if (isInitScale) return;
        Drawable d = getDrawable();
        if (d == null) return;
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        int dw = d.getIntrinsicWidth();
        int dh = d.getIntrinsicHeight();
        Log.e("main", "dw  " + dw + "dh  " + dh);
//        if (height > dh && width > dw) {
//            float scaleW = (float) width / (float) dw;
//            float scaleH = (float) height / (float) dh;
//            mInitScale = Math.min(scaleH, scaleW);
//        }
        float scaleW = (float) width / (float) dw;
        float scaleH = (float) height / (float) dh;
        if (dh > height && dw > width) {

        }
        mInitScale = Math.min(scaleH, scaleW);
        mMaxScale = mInitScale * 4;
        mCenterScale = mInitScale * 2;
        matrix.postTranslate((width - dw) / 2, (height - dh) / 2);
        matrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
        setImageMatrix(matrix);
        isInitScale = true;

        mScaleFocus[0] = width / 2;
        mScaleFocus[1] = height / 2;
    }

解释下 首先 拿到图片本身的宽高 和控件的宽高 做比较 然后 移动 并且缩放到 图片的中间 然后 初始化一个缩放中心 和双击的 宽高 和最大宽高

2)手势控制
 mGestureDetector = new GestureDetector(context, new TapCallback());
        mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleCallback());
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        mScaleGestureDetector.onTouchEvent(event);
        if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
            if (checkScrollBorder) {
                checkBorder();
                checkScrollBorder = false;
            }
        }
        return true;
    }

如上 初始化 手势工具类 在缩放的 过程中 如果 我们将要缩放的scale 小于原来的宽高的一半 直接跳过 否则 将图片的matrix 缩放 并设置

public boolean onScale(ScaleGestureDetector detector) {
            float factor = detector.getScaleFactor();
            if (getCurScale() * factor < 0.5f * mInitScale) {
                return true;
            }
            if (!isScale)
                isScale = true;
            mScaleFocus[0] = detector.getFocusX();
            mScaleFocus[1] = detector.getFocusY();

            matrix.postScale(factor, factor, detector.getFocusX(), detector.getFocusY());
            setImageMatrix(matrix);
            return true;
        }

然后是缩放结束

  @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            isScale = false;
            super.onScaleEnd(detector);
            if (getCurScale() > mMaxScale) {
                slowScale(mMaxScale);
            }
            if (getCurScale() < mInitScale) {
                resetScale();
            }
        }

如果 当前的scale 大于最大的scale 将会 重置为最大 如果小于 我的初始scale 将会重置为初始scale

这里介绍下慢动作缩放

 ValueAnimator animator = ValueAnimator.ofFloat(getCurScale(), target);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float curTarget = (float) animation.getAnimatedValue();
                float factor = curTarget / getCurScale();
                matrix.postScale(factor, factor, mScaleFocus[0], mScaleFocus[1]);
                setImageMatrix(matrix);
                if (animation.getAnimatedFraction() == 1) {
                    checkBorder();
                    if (mScaleEndListener != null) mScaleEndListener.onScaleEnd();
                    mScaleEndListener = null;
                }
            }
        });
        animator.setDuration(200).start();

如上 传入一个目标的scale 先初始化一个valueAnimator 在动画执行的过程中 一直设置缩放 来达到一个渐进的缩放

接下来是双击

@Override
        public boolean onDoubleTap(MotionEvent e) {
            float curScale = getCurScale();
            if (curScale < mCenterScale) {
                mScaleFocus[0] = e.getX();
                mScaleFocus[1] = e.getY();
                slowScale(mCenterScale);
            } else {
                resetScale();
            }
            return true;
        }

同样的调用slowScale方法

点击回调

 @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            if (mGestureListener != null) {
                mGestureListener.onSingleTapUp();
            }
            return true;
        }

滑动

 @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (Math.abs(distanceX) < mTouchSlop && Math.abs(distanceY) < mTouchSlop) {
                return true;
            }
            if (isScale) return true;//滑动的时候 如果在缩放 则 无操作
            ScaleImageView.this.onScroll(distanceX, distanceY);
            return false;
        }
protected void onScroll(float distanceX, float distanceY) {
        checkIntercept(distanceX);
        if (getCurScale() == mInitScale) {
            distanceY = 0;
        }
        float[] target = checkScroll(distanceX, distanceY);
        matrix.postTranslate(-target[0], -target[1]);
        setImageMatrix(matrix);
        checkScrollBorder = true;
    }

首先 我们看下checkIntercept 这个方法 是用来 阻止viewPage 拦截我的滑动的 因为 我可能处于放大的情况 此时 我需要 滑动我自己

private void checkIntercept(float dx) {
        RectF rectF = getMatrixRectF();
        float width = getWidth();
        float height = getHeight();
        if (rectF.height() > height || rectF.width() >= width) {
            if (rectF.right >= width && dx > 0) {
                getParent().requestDisallowInterceptTouchEvent(true);
            } else if (rectF.left <= 0 && dx < 0) {
                getParent().requestDisallowInterceptTouchEvent(true);
            } else {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
        }
    }

如上 首先 拿到 图片的矩形 (此矩阵为当前图片的内容范围 讲matrix 映射到一个rectF 即可)如果还没有滑过我显示的位置 则不允许父类拦截 。细心的你可能发现 还有个checkScroll 方法 我们来看下 这个方法

protected float[] checkScroll(float distanceX, float distanceY) {
        RectF rectF = getMatrixRectF();
        float width = getWidth();
        float height = getHeight();
        float damp = 4;
        if (rectF.width() >= width) {
            if (rectF.left > 0) {
                distanceX = distanceX / damp;
            }
            if (rectF.right < width) {
                distanceX = distanceX / damp;
            }
        }
        if (rectF.height() >= height) {
            if (rectF.top > 0) {
                distanceY = distanceY / damp;
            }
            if (rectF.bottom < height) {
                distanceY = distanceY / damp;
            }
        }
        if (rectF.width() < width) {
            distanceX = distanceX / damp;
        }
        if (rectF.height() < height) {
            distanceY = distanceY / damp;
        }
        float[] result = new float[2];
        result[0] = distanceX;
        result[1] = distanceY;
        return result;
    }

这个是个阻尼 当滑动的位置 已经 脱离了 图片的边界 则给他一个阻尼

最后就是边界检测了 我们开看下 这个方法

protected void checkBorder() {
        RectF rectF = getMatrixRectF();

//        Log.e("main", rectF.toString());
//        Log.e("main", "width " + rectF.width());
        float dx = 0;
        float dy = 0;
        float width = getWidth();
        float height = getHeight();
        if (rectF.width() >= width) {
            if (rectF.left > 0) {
                dx = -rectF.left;
            }
            if (rectF.right < width) {
                dx = width - rectF.right;
            }
        }
        if (rectF.height() >= height) {
            if (rectF.top > 0) {
                dy = -rectF.top;
            }
            if (rectF.bottom < height) {
                dy = height - rectF.bottom;
            }
        }

        if (rectF.width() < width) {
            dx = width / 2f + rectF.width() / 2f - rectF.right;
        }
        if (rectF.height() < height) {
            dy = height / 2f + rectF.height() / 2f - rectF.bottom;
        }

        matrix.postTranslate(dx, dy);
        setImageMatrix(matrix);
        //slowTranslate(dx,dy);
    }

如上 拿到当前的图片范围 与图片宽高做比较 这里 画了一张当图片小于空间的宽高 的时候的范围 希望能帮助理解

这里写图片描述

3)增加对长图的支持

主要支持为长图截屏 ,主要使用 BitmapRegionDecoder 类 一次解析一个矩形的范围,主要代码 为

mImageRect.offset((int) distanceX, (int) distanceY);
        checkSelf();
        setImageBitmap(mDecoder.decodeRegion(mImageRect, mOptions));    

如上 此类 继承自ScaleImageView 主要在onScroll的时候 移动矩形 并且不断的设置。

至此 图片缩放分析完毕,当然 各种很多细节 还需要仔细参考代码。

6 、图片剪切

1)绘制剪切范围

如图 最我们应该以图片的范围 为基准 这是图片的初始 rect 那么 我们需要绘制的矩形应该为left=范围.left+线宽/2,其它边界也是一样的方法 算出来

然后是边角 边角为矩形的范围 偏移线宽的一半 (参考代码)

然后是4条横线的绘制

这里写图片描述

private void drawProfile(Canvas canvas) {
        mPaint.setStrokeWidth(mProfileWidth);
        //移动四角线宽
        float offset = mProfileWidth / 2;
        float left = mLeft - offset;
        float top = mTop - offset;
        float right = mRight + offset;
        float bottom = mBottom + offset;
        mProfile = new RectF(left, top, right, bottom);
        canvas.drawRect(mProfile, mPaint);
    }

如上 先是 绘制矩形

  Path path = new Path();

        int offset = mProfileWidth + mCornerWidth / 2;
        canvas.drawPath(path, mPaint);
        float left = mLeft - offset;
        float top = mTop - offset;
        float right = mRight + offset;
        float bottom = mBottom + offset;

        path.moveTo(left, top + mCornerSize);
        path.lineTo(left, top);
        path.lineTo(left + mCornerSize, top);
        canvas.drawPath(path, mPaint);

然后是绘制一条边框 其它3个边框以此类推
这里写图片描述
将会得到如上图所示的矩形框(当前 已经对 图片进行了0.8f的缩放 )

当然 还有矩形的最小边界 和最大边界的控制 都是放在滑动中检测的限于篇幅限制,这里点到为止

首先带down 事件 确认我是不是应该移动我的矩形 ,即按下的位置落在我的四个边框

 private boolean shouldMove(MotionEvent e) {
        float x = e.getX();
        float y = e.getY();
        if (x > mProfile.right || x < mProfile.left || y > mProfile.bottom || y < mProfile.top) {
            //在矩形的外面
            return false;
        }
        if (x < mProfile.left + mCornerSize && y < mProfile.top + mCornerSize) {
            mCurScrollRange = LEFT_TOP;
            return true;
        }
        if (x > mProfile.right - mCornerSize && y < mProfile.top + mCornerSize) {
            mCurScrollRange = RIGHT_TOP;
            return true;
        }
        if (x < mProfile.left + mCornerSize && y > mProfile.bottom - mCornerSize) {
            mCurScrollRange = LEFT_BOTTOM;
            return true;
        }
        if (x > mProfile.right - mCornerSize && y > mProfile.bottom - mCornerSize) {
            mCurScrollRange = RIGHT_BOTTOM;
            return true;
        }
        return false;
    }

如下 改变矩形的范围 重回,过程中 检测边界 不应该超出初始边界,最小不应该超过边框合并的 位置

 switch (mCurScrollRange) {
                case LEFT_TOP:
                    mLeft -= distanceX;
                    mTop -= distanceY;
                    break;
                case LEFT_BOTTOM:
                    mLeft -= distanceX;
                    mBottom -= distanceY;
                    break;
                case RIGHT_TOP:
                    mRight -= distanceX;
                    mTop -= distanceY;
                    break;
                case RIGHT_BOTTOM:
                    mRight -= distanceX;
                    mBottom -= distanceY;
                    break;
            }
            checkBorder(distanceX, distanceY);
            invalidate();

2)剪切

传入当前矩形的拖动范围

public Bitmap clipImage(Rect rect) {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        draw(canvas);
        return Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
    }

如上 调用draw 方法 就把 当前图片 draw到了我的画布上了!

7. 图片涂鸦

1)原型颜色绘制

这里写图片描述

首先放置一排颜色提供选择,其实很简单 画一个圈就好了 选中的时候圈大 未选中的时候圈小 然后 一个大圈套一个小圈

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int centerX = getMeasuredWidth() / 2;

        mPaint.setColor(Color.WHITE);
        canvas.drawCircle(centerX, centerX, centerX, mPaint);

        mPaint.setColor(mColor);
        canvas.drawCircle(centerX, centerX, centerX - offset, mPaint);
    }
2) 涂写

很简单 沿线绘制即可

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isInEditStatus) {
            for (PathInfo pathInfo : mPaths) {
                Path path = pathInfo.getPath();
                if (pathInfo == getLastPath()) {
                    path.lineTo(mCurX, mCurY);
                }
                mPaint.setColor(pathInfo.getColor());
                canvas.drawPath(path, mPaint);
            }
        }
    }

然后同样的方法 ,画出当前的 bitmap 就大功告成了!

其他的细节

1 缓存池 使用静态哦 这样不用每次 都创建图片的缓存池
2. 注意刷新viewpager的方法

if (resultCode == RESULT_OK) {
                String path = data.getStringExtra("path");
                paths.set(mPreviewVp.getCurrentItem(), path);
                ScaleImageView image = (ScaleImageView) mPreviewVp.getChildAt(mPreviewVp.getCurrentItem());
                Bitmap result = Utils.compress(path, mPreviewVp.getWidth(), mPreviewVp.getHeight());
                image.setImageBitmap(result);
                //mPreViewImages.set(mPreviewVp.getCurrentItem(), image);
                mPreviewVp.getAdapter().notifyDataSetChanged();
                mThumbList.getAdapter().notifyItemChanged(mPreviewVp.getCurrentItem());
            }

然后惯性滑动的代码有些bug 在手势的fling 方法中 已将其注释,待后续解决

大功告成

github 地址 https://github.com/fanyaopeng/MultiPicturesSelector 欢迎大家提出bug!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值