Android自定义ImageView(上)之多点触控放大缩小,
[TOC]
概述
这是一个可以设置成圆角或者圆角矩形的ImageView,并且可以设置是否支持多点触控放大,缩小,旋转图片,双击放大缩小的自定义的控件。还有一个仿刮刮卡效果的自定义View。
效果展示
效果展示
录制的视频5.4M,可能打不开得下下来看。
相关知识点
-
android matrix 最全方法详解与进阶(完整篇)
这里用于图形变换如:放大缩小,平移,旋转 -
Android的事件分发机制
这里用于解决ImageView与ViewPager嵌套时的事件冲突 -
xFermode的原理及使用
这里用于设置ImageView内图片的形状:圆形和圆角矩形
还有自定义View的刮刮卡效果 -
GestureDetector与ScaleGestureDetector入门
Google的扩展的手势监听api - 其他(来不及了,要开车了!默认你懂了)
关键代码和注意事项
初始化
因为我们是使用matirx来做图形的变化,所以要设置 setScaleType(ScaleType.MATRIX);图形变换的方法setImageMatrix(Matrix matrix);才会生效。
接着我们在xml中配置ImageView
<com.example.administrator.imagetest.MyImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@mipmap/test" />
问题来了,图片太大显示不全,而且位于控件的左上角。我想让他像微信一样,自动调整到控件中心点,并且等比例缩放到一个屏幕放的下。这时我们就要在控件图像被绘制出来的时候,调整图片大小和位置
实现implements ScaleGestureDetector.OnScaleGestureListener接口
/**
* 控件被加载到窗口时,监听View变化
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
}
/**
* 控件被销毁时,关闭监听
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
初始化图片大小和位置:
/**
* 当View发生改变的时候,会调用这个监听,可能多次调用,所以要加判断
*/
@Override
public void onGlobalLayout() {
if (once) {
initView();
once = false;
}
}
/**
* 初始化图片大小,位置
*/
private void initView() {
Drawable d = getDrawable();
if (d == null)
return;
//获取imageview宽高
int width = getWidth();
int height = getHeight();
//获取图片宽高
startWidth = startWidth > 0 ? startWidth : d.getIntrinsicWidth();
startHeight = startHeight > 0 ? startHeight : d.getIntrinsicHeight();
float scale = 1.0f;
//如果图片的宽或高大于屏幕,缩放至屏幕的宽或者高
if (startWidth > width && startHeight <= height) {
scale = (float) width / startWidth;
}
if (startHeight > height && startWidth <= width) {
scale = (float) height / startHeight;
}
//如果图片宽高都大于屏幕,按比例缩小
if (startWidth > width && startHeight > height) {
scale = Math.min((float) startWidth / width, (float) startHeight / height);
}
//如果图片宽高都小于屏幕,选取差比较小的那个比例,放大
if (startWidth < width && startHeight < height) {
scale = Math.min((float) width / startWidth, (float) height / startHeight);
}
//将图片移动至屏幕中心
matrix.postTranslate((width - startWidth) / 2, (height - startHeight) / 2);
//然后再放大
matrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
firstScale = firstScale > 0 ? firstScale : scale;
setImageMatrix(matrix);
getMatrixRectF();
}
这样图片就可以在整个控件中居中,并且完全显示了。
放大缩小
实现ScaleGestureDetector.OnScaleGestureListener接口,
在onScale(ScaleGestureDetector detector)中做放大缩小的逻辑处理。
@Override
public boolean onScale(ScaleGestureDetector detector) {
//这个放大缩小是每次进行细微的变化,通过频繁变化,来改变图片大小
//通过与imageView本身的宽高进行限制,最大不过4倍,最小不过四分之一
//放大
if (detector.getScaleFactor() >= 1) {
if (Math.min(getMatrixRectF().bottom - getMatrixRectF().top,
getMatrixRectF().right - getMatrixRectF().left) / getWidth() <= MAX_SCALE) {
matrix.postScale(detector.getScaleFactor(),
detector.getScaleFactor(), getWidth() / 2, getHeight() / 2);
setImageMatrix(matrix);
getMatrixRectF();
}
} else {
//缩小
if (getWidth() / Math.min(getMatrixRectF().bottom - getMatrixRectF().top,
getMatrixRectF().right - getMatrixRectF().left) <= MIN_SCALE) {
matrix.postScale(detector.getScaleFactor(), detector.getScaleFactor(),
getWidth() / 2, getHeight() / 2);
setImageMatrix(matrix);
getMatrixRectF();
}
}
return true;
}
这时,我们发现放大的时候,图片超过了屏幕范围,却不能放大,这个体验超级差的。这个时候,就要实现GestureDetector.OnGestureListener,在onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)方法中处理滑动事件了:
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//判断宽度或者高度有一个大于控件宽高的
if (((getMatrixRectF().right - getMatrixRectF().left) > getWidth()
|| (getMatrixRectF().bottom - getMatrixRectF().top) > getHeight())) {
//滑动到横坐标边界,将事件交给viewpager处理
if(amendment(-distanceX, -distanceY)[0]==0){
//可以滑动,拦截Viewpager事件
getParent().requestDisallowInterceptTouchEvent(false);
}else{
//可以滑动,拦截Viewpager事件
getParent().requestDisallowInterceptTouchEvent(true);
}
matrix.postTranslate(amendment(-distanceX, -distanceY)[0],
amendment(-distanceX, -distanceY)[1]);
} else {
getParent().requestDisallowInterceptTouchEvent(false);//不能滑动,viewpager处理事件
}
setImageMatrix(matrix);
return false;
}
/**
* 滑动前判断滑动是否会造成越界,并对最终滑动距离进行修正
*/
private float[] amendment(float distanceX, float distanceY) {
float[] dis = new float[]{distanceX, distanceY};
//先判断图片的宽高是否大于屏幕,大于屏幕才能滑动
if ((getMatrixRectF().bottom - getMatrixRectF().top) > getHeight()) {
//判断Y轴上图片的顶部加上滑动的距离是否大于屏幕的顶部
if (getMatrixRectF().top + dis[1] > getTop()) {
//如果超过了,重新设置滑动距离,使之不超过边界
dis[1] = getTop() - getMatrixRectF().top;
}
if (getMatrixRectF().bottom + dis[1] < getBottom()) {
dis[1] = getBottom() - getMatrixRectF().bottom;
}
} else {
dis[1] = 0;
}
if ((getMatrixRectF().right - getMatrixRectF().left) > getWidth()) {
if (getMatrixRectF().left + dis[0] > getLeft()) {
dis[0] = getLeft() - getMatrixRectF().left;
}
if (getMatrixRectF().right + dis[0] < getRight()) {
dis[0] = getRight() - getMatrixRectF().right;
}
} else {
dis[0] = 0;
}
return dis;
}
/**
* 根据当前图片的Matrix获得图片的范围
* @return
*/
private RectF getMatrixRectF() {
Drawable d = getDrawable();
if (null != d) {
rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
matrix.mapRect(rect);
}
return rect;
}
这里的代码比较多,可实际上滑动图片也就是matrix.postTranslate();这个方法而已。只要是还要做一个图片是否可以滑动的判断,是否滑动越界,还有处理嵌套ViewPager时的事件分发冲突处理。
双击放大缩小
这里我们不能设置成第一次双击放大,第二次双击缩小,这样子的。正常我们是会设置最大倍数和最小倍数的,如果用户先用触控把图片拉到最大,然后双击图片。这个时候,图片就没有反应了。所以要根据当前的一个放大倍率来决定是要放大还是缩小图片。
/**
* 双击放大缩小
*/
private void doubleClick() {
gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
scaleType();
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
});
}
/**
* 双击放大缩小的动画
*/
private void scaleType() {
final float scale = getScale() / firstScale;
//想要放大缩小的中心是在屏幕的中心,那么图片的中心也要在屏幕中心,
// 这是就得通过计算偏移量,把图片先移动到屏幕中心来
if (scale >= 1 && scale <= 2) {
matrix.postTranslate(getWidth() / 2 - getMatrixRectF().centerX(),
getHeight() / 2 - getMatrixRectF().centerY());
matrix.postScale(MAX_SCALE / scale, MAX_SCALE / scale,
getWidth() / 2, getHeight() / 2);
setImageMatrix(matrix);
getMatrixRectF();
} else {
matrix.postTranslate(getWidth() / 2 - getMatrixRectF().centerX(),
getHeight() / 2 - getMatrixRectF().centerY());
matrix.postScale(1 / scale, 1 / scale, getWidth() / 2, getHeight() / 2);
//将图片移动至屏幕中心
setImageMatrix(matrix);
getMatrixRectF();
}
}
注意啦,敲黑板!
这段处理双击逻辑的代码,在缩放之前添加了一个平移的方法,这是因为如果我们在放大以后,移动图片再双击缩小的话,图片的位置会改变,不再是屏幕的中心(原来的位置是在屏幕的中心)而我们在缩放的时候是以屏幕的中心位置为缩放中心进行缩放的,所以图片的位置会发生偏移。但是我们通过触控放大缩小图片的时候不会出现这种情况,是因为move事件的频率快,每次变化只有一点点,这个时候平移和缩放是在交替执行的,所以能够自动修正到正确的位置
快十二点了,剩下的周五再写吧。下方有这个demo的源码地址(效率好低)
项目地址链接
个人公众号
不只是技术文章哦,快关注我吧。
搜索公众号:kedasay