前言
放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下:
- 侧边区域观测要移动Shader或者在指定位置裁剪图像
- 本文效果是移动区域,但是为了保证图片能尽可能对齐,需要将放大的图片向左上角偏移。
本文和上一篇《手电筒照亮效果》一样,如果没看过的先看上一篇,方便你理解本篇,因为同样的原理不会在这篇重新提及或者过多提及,都是局部区域效果实现。
效果预览
滑动放大效果
窥视效果
方法镜滑动放大实现方法
使用Shader作为载体
首先要做的是将图片放大,放大之后,我们可以利用Path裁剪图片或者Shader向裁剪区域绘制,这里我们依然使用Shader,毕竟优点很多,这里我们主要要实现2个目的。
- Shader载入Bitmap,放大1.2倍
- Shader向左上角偏移,对齐图片中心
java
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 做下偏移
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}
事件处理
其实处理事件有很多简便的方法,但是首先得拦截事件,Android种拦截事件的方法很多,clickable就是其中之一
java
setClickable(true); //触发hotspot
拦截按压移动事件,这里我们使用 HotSpot 机制,其实就是触点,西方人命名习惯使用HotSpot,通过下面就能处理事件,连onTouchEvent我们都不用搭理。
java
@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}
@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}
裁剪Canvas区域为原图区域
为什么要裁剪Canvas区域内,主要是因为你的图片并不一定能完全填充整个View,但是你使用的TileMode肯定是CLAMP,这会使得放大镜中图像的边缘拉长,现象很奇怪,反正你可以去掉试试。另外说一下,Android中似乎新增加了一种TileMode,不过还没来得及试一下。
java
int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.restoreToCount(save);
绘制核心逻辑
在核心逻辑中,我们有一步要绘制区域填充颜色,主要原因是非透明区域的绘制会导致出现透视效果。
java
int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);
canvas.restoreToCount(save);
放大镜窥视效果
其实两者代码没有多大区别,滑动放大效果主要是移动镜子,而窥视效果镜子不动,使用移动图片的方式实现。
位置计算 & 绘制
固定镜子中心在右下角
java
//放大平移时需要偏移的距离
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;
//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;
图像平移距离
scss
(mirrorCenterX - x)
(mirrorCenterY - y)
矩阵变换,平移事件点位置图像到右下角圆的中心
scss
//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);
绘制镜子
ini
int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);
canvas.restoreToCount(save);
总结
本篇和之前的很多篇文章一样,都是实现Canvas图片绘制,很复杂的效果我们没有涉及到,但是在这些文章中,都会有各种各样的问题和思考。总之,我们要善于利用矩阵和设计思想,绘制我们的想象。
全部代码
按照惯例,提供全部代码
滑动放大代码
java
public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;
public ScaleBigView(Context context) {
this(context, null);
}
public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);
}
private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}
int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);
canvas.restoreToCount(save);
}
@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}
@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
}
窥视镜效果
scss
public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;
public ScaleBigView(Context context) {
this(context, null);
}
public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);
}
private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
}
//放大平移
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;
//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;
//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);
int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);
canvas.restoreToCount(save);
}
@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}
@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
}