2019-09-03 解析仿照ins视图拖动效果

之前就一直觉得INS的视图拖动缩放效果很酷炫,特地去网上搜索了下(本着不重复造轮子的想法),找到一个比较合适的。
地址是:https://github.com/okaybroda/ImageZoom

首先分析ins的视图拖动效果,双指放大以后,视图View就会悬浮,跟着手指移动,进行缩放,效果是相当的酷炫。
所以可以分成几步来梳理:
1.双指放上去以后有缩放动作,取出视图View
2.根据双指的操作,对视图View进行放大缩小以及拖动
3.当屏幕上的手指数小于2时,将视图View放回原来的地方

基本上就是以上4个步骤,那就逐个分析。

一 取出视图View

用过ins的同学都知道,不管是视频还是图片,都是可以拿出来缩放的,仔细观察可以发现当视图View被拿出来的时候,原先的位置是一片空白的,随后的移动缩放界面,除了把原来的位置上的View抠出来以后,并不影响其他,那么猜测应该是新建了一个Act设置成透明的全屏背景加上原来的View进行展示或者是一个全屏的Dialog。那么如何把View抠出来呢?

ImageZoom这个项目里首先是将需要绑定的View进行setTag,回头再根据这个tag找到对应的View

/**
     * Finds the view that has the R.id.zoomable tag and also contains the x and y coordinations
     * of two pointers
     *
     * @param event MotionEvent that contains two pointers   带两个手指的手势
     * @param view  View to find in  也就是我们需要缩放的View的ViewGroup
     * @return zoomable View      我们需要缩放的View
     */
private View findZoomableView(MotionEvent event, View view) {
        if (view instanceof ViewGroup) {
            //首先获得viewGoup,以及他的子View的个数
            ViewGroup viewGroup = (ViewGroup) view;
            int childCount = viewGroup.getChildCount();

            //获得两个手指所在坐标点的对象pointerCoords1,pointerCoords2 
            MotionEvent.PointerCoords pointerCoords1 = new MotionEvent.PointerCoords();
            event.getPointerCoords(0, pointerCoords1);  

            MotionEvent.PointerCoords pointerCoords2 = new MotionEvent.PointerCoords();
            event.getPointerCoords(1, pointerCoords2);

            //采取递归的方法寻找目标view
            for (int i = 0; i < childCount; i++) {
                View child = viewGroup.getChildAt(i);

                if (child.getTag(R.id.unzoomable) == null) {
                    Rect visibleRect = new Rect();
                    int location[] = new int[2];
                    child.getLocationOnScreen(location);
                    visibleRect.left = location[0];
                    visibleRect.top = location[1];
                    visibleRect.right = visibleRect.left + child.getWidth();
                    visibleRect.bottom = visibleRect.top + child.getHeight();

                    if (visibleRect.contains((int) pointerCoords1.x, (int) pointerCoords1.y) &&
                            visibleRect.contains((int) pointerCoords2.x, (int) pointerCoords2.y)) {
                        if (child.getTag(R.id.zoomable) != null)
                            return child;
                        else
                            return findZoomableView(event, child);
                    }
                }
            }
        }

        return null;
    }

找到目标View以后,

                    // 计算出view在界面上原来的坐标
                    originalXY = new int[2];
                    view.getLocationInWindow(originalXY);

                    // 新建一个FrameLayout用来作为view的父布局
                    FrameLayout frameLayout = new FrameLayout(view.getContext());

                    // 这个视图是用来当用户缩放的时候,控制背景的透明度的
                    darkView = new View(view.getContext());
                    darkView.setBackgroundColor(Color.BLACK);
                    darkView.setAlpha(0f);

                    // 将darkView添加到父布局当中
                    frameLayout.addView(darkView, new FrameLayout.LayoutParams(
                            FrameLayout.LayoutParams.MATCH_PARENT,
                            FrameLayout.LayoutParams.MATCH_PARENT));

                    // 创建一个dialog,用来展示缩放布局
                    dialog = new Dialog(activity,
                            android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
                    dialog.addContentView(frameLayout,
                            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                    ViewGroup.LayoutParams.MATCH_PARENT));
                    dialog.show();

                    // 获取zoomView的父布局,并且得到zoomview在布局中的index,以及zoomview的    
                    // layoutparams
                    parentOfZoomableView = (ViewGroup) zoomableView.getParent();
                    viewIndex = parentOfZoomableView.indexOfChild(zoomableView);
                    this.zoomableViewLP = zoomableView.getLayoutParams();

                    // 将之前得到的参数设置到zoomview里面
                    zoomableViewFrameLP = new FrameLayout.LayoutParams(
                            view.getWidth(), view.getHeight());
                    zoomableViewFrameLP.leftMargin = originalXY[0];
                    zoomableViewFrameLP.topMargin = originalXY[1];

                    // 创建一个临时的view,用来代替zoomview原来的位置
                    placeholderView = new View(activity);

                    // 把placeholdeView的背景设置成zoomview的缓存视图
                    // 这样可以避免在添加和删除视图时,造成闪烁
                    zoomableView.setDrawingCacheEnabled(true);

                    BitmapDrawable placeholderDrawable = new BitmapDrawable(
                            activity.getResources(),
                            Bitmap.createBitmap(zoomableView.getDrawingCache()));
                    if (Build.VERSION.SDK_INT >= 16) {
                        placeholderView.setBackground(placeholderDrawable);
                    } else {
                        placeholderView.setBackgroundDrawable(placeholderDrawable);
                    }

                    // placeholderView暂时替代zoomview
                    parentOfZoomableView.addView(placeholderView, zoomableViewLP);

                    // 在添加到Framelayout之前,要先把zoomview从父布局移除
                    parentOfZoomableView.removeView(zoomableView);
                    frameLayout.addView(zoomableView, zoomableViewFrameLP);

                    // 利用zoomview的post方法移除placeholderview的缓存视图
                    zoomableView.post(new Runnable() {
                        @Override
                        public void run() {
                            if (dialog != null) {
                                if (Build.VERSION.SDK_INT >= 16) {
                                    placeholderView.setBackground(null);
                                } else {
                                    placeholderView.setBackgroundDrawable(null);
                                }

                                zoomableView.setDrawingCacheEnabled(false);
                            }
                        }
                    });

                    // 创建对象pointerCoords1,pointerCoords2,得到两个手指的坐标位置
                    MotionEvent.PointerCoords pointerCoords1 = new MotionEvent.PointerCoords();
                    ev.getPointerCoords(0, pointerCoords1);

                    MotionEvent.PointerCoords pointerCoords2 = new MotionEvent.PointerCoords();
                    ev.getPointerCoords(1, pointerCoords2);

                    // 计算出两个坐标点之间的距离,后面可以用来计算缩放大小
                    originalDistance = (int) getDistance(pointerCoords1.x, pointerCoords2.x,
                            pointerCoords1.y, pointerCoords2.y);

                    // 计算出两个坐标对应的矩形的中心点位,后面可以根据这个点位来移动视图
                    twoPointCenter = new int[]{
                            (int) ((pointerCoords2.x + pointerCoords1.x) / 2),
                            (int) ((pointerCoords2.y + pointerCoords1.y) / 2)
                    };

                    //计算出手指移动的距离,用来缩放移动视图
                    pivotX = (int) ev.getRawX() - originalXY[0];
                    pivotY = (int) ev.getRawY() - originalXY[1];

                    sendZoomEventToListeners(zoomableView, true);
                    return true;

上面的注释已经写得很详细了,当我们的两个手指放在屏幕上时,第一次寻找zoomview时,简单来说,就是抠出zommview,放到新的布局里面,然后创建一个view,临时代替zoomview原来的位置.下面我们看看zoomview找到以后,具体又是怎么操作进行缩放移动的。

                //首先还是先拿到两个手指的位置,后面用来算出两个手指的中心点位newCenter
                MotionEvent.PointerCoords pointerCoords1 = new MotionEvent.PointerCoords();
                ev.getPointerCoords(0, pointerCoords1);

                MotionEvent.PointerCoords pointerCoords2 = new MotionEvent.PointerCoords();
                ev.getPointerCoords(1, pointerCoords2);

                int[] newCenter = new int[]{
                        (int) ((pointerCoords2.x + pointerCoords1.x) / 2),
                        (int) ((pointerCoords2.y + pointerCoords1.y) / 2)
                };

                //通过跟刚才初始距离的比较,计算出现在的缩放百分比
                int currentDistance = (int) getDistance(pointerCoords1.x, pointerCoords2.x,
                        pointerCoords1.y, pointerCoords2.y);
                double pctIncrease = (currentDistance - originalDistance) / originalDistance;

                //设置zoomview的缩放中心点为刚才手指头刚放上去的中心点
                zoomableView.setPivotX(pivotX);
                zoomableView.setPivotY(pivotY);

                //设置zoomview的缩放比例
                zoomableView.setScaleX((float) (1 + pctIncrease));
                zoomableView.setScaleY((float) (1 + pctIncrease));

                //更新zoomview的边距
                updateZoomableViewMargins(newCenter[0] - twoPointCenter[0] + originalXY[0],
                        newCenter[1] - twoPointCenter[1] + originalXY[1]);

                //设置darkview的透明度
                darkView.setAlpha((float) (pctIncrease / 8));

找到zoomview以后的工作量已经不多了,就是通过计算出两个手指头最新的距离,跟原始距离进行比较,算出缩放的百分比以后,利用view的自带方法,进行缩放,并且设置darkview的透明度。此时手指拖动抠出目标view,并且对view进行缩放已经分析完成了。接下来就分析手指头释放以后,将view设置回原来的位置,并且有一个回弹的动画效果。

//首先肯定是zoomview不为空,并且动画需要展示的情况,才允许接下来的操作
if (zoomableView != null && !isAnimatingDismiss) {
                isAnimatingDismiss = true;

                //先拿到zoomview的最终缩放大小,边距,以及darkview的最终透明度
                final float scaleYStart = zoomableView.getScaleY();
                final float scaleXStart = zoomableView.getScaleX();
                final int leftMarginStart = zoomableViewFrameLP.leftMargin;
                final int topMarginStart = zoomableViewFrameLP.topMargin;
                final float alphaStart = darkView.getAlpha();

                //下面就是没有进行缩放之前的参数,包括缩放大小,边距,darkview的透明度。
                final float scaleYEnd = 1f;
                final float scaleXEnd = 1f;
                final int leftMarginEnd = originalXY[0];
                final int topMarginEnd = originalXY[1];
                final float alphaEnd = 0f;

                //利用valueAnimator展示动画,就是根据动画的进行的百分比,进行百分比的缩放等回弹
                //相信有一点基础的Android开发同学都是懂得的,就不过多分析了
                final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
                          
                valueAnimator.setDuration(activity.getResources().getInteger
                                                                                   (android.R.integer.config_shortAnimTime));
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                    @Override
                    public void onAnimationUpdate(ValueAnimator valueAnimator) {
                        float animatedFraction = valueAnimator.getAnimatedFraction();
                        if (zoomableView != null) {
                            updateZoomableView(animatedFraction, scaleYStart, scaleXStart,
                                    leftMarginStart, topMarginStart,
                                    scaleXEnd, scaleYEnd, leftMarginEnd, topMarginEnd);
                        }

                        if (darkView != null) {
                            darkView.setAlpha(((alphaEnd - alphaStart) * animatedFraction) +
                                    alphaStart);
                        }
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationCancel(Animator animation) {
                        super.onAnimationCancel(animation);
                        end();
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        end();
                    }

                    //当前view已经缩放结束,恢复初始的一些状态,并且
                    void end() {
                        if (zoomableView != null) {
                            updateZoomableView(1f, scaleYStart, scaleXStart,
                                    leftMarginStart, topMarginStart,
                                    scaleXEnd, scaleYEnd, leftMarginEnd, topMarginEnd);
                        }
                        dismissDialogAndViews();

                        valueAnimator.removeAllListeners();
                        valueAnimator.removeAllUpdateListeners();
                    }
                });
                valueAnimator.start();

                return true;
            }

到这里基本就已经分析完了,还有最后一个方法要分析dismissDialogAndViews(),隐藏dialog并且恢复zoomview在原来act上面的位置。

        //回调通知缩放结束了
        sendZoomEventToListeners(zoomableView, false);

        if (zoomableView != null) {
            //将zoomview从framelayout中抠出来,重新添加到之前的父布局当中去
            zoomableView.setVisibility(View.VISIBLE);
            zoomableView.setDrawingCacheEnabled(true);

            BitmapDrawable placeholderDrawable = new BitmapDrawable(
                    zoomableView.getResources(),
                    Bitmap.createBitmap(zoomableView.getDrawingCache()));
            if (Build.VERSION.SDK_INT >= 16) {
                placeholderView.setBackground(placeholderDrawable);
            } else {
                placeholderView.setBackgroundDrawable(placeholderDrawable);
            }

            ViewGroup parent = (ViewGroup) zoomableView.getParent();
            parent.removeView(zoomableView);
            //利用刚才拿到的viewIndex,准确的添加回原来的位置
            this.parentOfZoomableView.addView(zoomableView, viewIndex, zoomableViewLP);
            this.parentOfZoomableView.removeView(placeholderView);
            final View finalZoomView = zoomableView;
            zoomableView.setDrawingCacheEnabled(false);
            dismissDialog();
            finalZoomView.invalidate();
        } else {
            dismissDialog();
        }

        isAnimatingDismiss = false;

终于分析完了,其他的一些相信都能看的懂,有疑问的地方的再留言,大家一起讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容