之前就一直觉得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;
终于分析完了,其他的一些相信都能看的懂,有疑问的地方的再留言,大家一起讨论。