本篇文章为利用Matrix自定义View三部曲的第一部曲。java
虽然Android内置了许多View供开发者组合和使用,但其多样性仍是不足,在不少场景或功能需求下,Android原生自带的控件并不足以实现需求,这时咱们就须要自定义知足咱们需求的View。git
本文会讲解一个自定义View的设计和开发过程,在阅读以前但愿你们有最基础的自定义View的知识,以及Matrix
类的基本使用。github
在不少图片社交的应用,例如Lofter、Play、In等应用中,都会有添加各类可爱的贴图到图片上的功能,而后咱们能够对图片进行移动、旋转、缩放、翻转之类的操做,本文制做的View正是为了实现这个功能。最终咱们将要实现的效果以下图:canvas
项目地址:github.com/wuapnjie/St…ide
要实现这样的效果,咱们确定须要对图片进行操做,在自定义的View中,咱们能够在onDraw()
方法将咱们的图片(一般为Bitmap
)画到View
上。post
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap,matrix,paint);
}复制代码
drawBitmap()
方法有许多重载方法,可是利用Matrix来控制画在View上的图片是最灵活最简单的。(不熟悉Matrix类能够先去了解下,这里就不介绍基础的知识了)spa
利用Matrix
能够方便的控制图片的位置,旋转角度,缩放比。设计
再看咱们的功能,用不一样的手势来操做图片,既然利用Matrix
能够操做图片,那么咱们只须要在View的onTouchEvent()
方法中监听不一样的手势操做,再对其Matrix进行变换,重绘View便可。整个思路流程就很清楚了。调试
有了思路,那么咱们就要来考虑咱们应该怎么样组织代码,怎么样设计代码的结构。固然这个View并不复杂,设计起来也不复杂。rest
首先,对于贴纸功能,在没有一张贴纸时就只显示一张图片,而这个功能ImageView已经为咱们实现了,因而StickerView应该继承自ImageView,而且重写onDraw()
和onTouchEvent()
方法。
其次,由于一张图片上能够添加多张贴纸,而每一张贴纸都须要一个Matrix来控制其相关变换,因此咱们能够设计一个类封装一下,方便对贴纸的操做。
public abstract class Sticker {
protected Matrix mMatrix;
public abstract void draw(Canvas canvas);
……
}复制代码
由于贴纸多是Bitmap,也就是普通的图片,可是咱们也能够添加气泡啊,标签啊之类的自定义的Drawable,
固然也多是各类图形,为了其扩展性,这里将Sticker类抽象。
扩展的DrawableSticker
public class DrawableSticker extends Sticker {
private Drawable mDrawable;
private Rect mRealBounds;
……
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.concat(mMatrix);
mDrawable.setBounds(mRealBounds);
mDrawable.draw(canvas);
canvas.restore();
}
……
}复制代码
那么大体的结构就肯定了,在View的onTouchEvent()
中,咱们根据手势改变Sticker的Matrix,并在onDraw()
方法中将Sticker画出。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
……
sticker.draw(canvas);
……
}复制代码
在有了思路和一个结构后,大体已经成功了一半,接下来就是一个个功能的实现,和一遍遍的调试了。
因为咱们能够添加不止一个Sticker,因此咱们的StickerView须要保有对全部添加的Sticker应用,这里能够用一个List集合来储存。而对于当前正在操做的Sticker引用须要额外储存。
由于对于不一样的手势,咱们所作出的操做不一样,那么咱们须要在内部声明全部存在的状态和一个当前状态
public class StickerView extends ImageView {
private enum ActionMode {
NONE, //nothing
DRAG, //drag the sticker with your finger
ZOOM_WITH_TWO_FINGER, //zoom in or zoom out the sticker and rotate the sticker with two finger
ZOOM_WITH_ICON, //zoom in or zoom out the sticker and rotate the sticker with icon
DELETE, //delete the handling sticker
FLIP_HORIZONTAL //horizontal flip the sticker
}
private ActionMode mCurrentMode = ActionMode.NONE;
private List<Sticker> mStickers = new ArrayList<>();
private Sticker mHandlingSticker;
……
}复制代码
接下来就是一个一个功能实现,但确定的是,最早须要实现的就是将贴纸添加进来的方法。
实现起来也很简单,这里就是new一个Sticker对象,并把它加入到咱们的List中并重绘,注意,咱们默认将Sticker缩放至原来的一半,并放在StickerView中央。
public void addSticker(Drawable stickerDrawable) {
Sticker drawableSticker = new DrawableSticker(stickerDrawable);
float offsetX = (getWidth() - drawableSticker.getWidth()) / 2;
float offsetY = (getHeight() - drawableSticker.getHeight()) / 2;
drawableSticker.getMatrix().postTranslate(offsetX, offsetY);
float scaleFactor;
if (getWidth() < getHeight()) {
scaleFactor = (float) getWidth() / stickerDrawable.getIntrinsicWidth();
} else {
scaleFactor = (float) getHeight() / stickerDrawable.getIntrinsicWidth();
}
drawableSticker.getMatrix().postScale(scaleFactor / 2, scaleFactor / 2, getWidth() / 2, getHeight() / 2);
mHandlingSticker = drawableSticker;
mStickers.add(drawableSticker);
invalidate();
}复制代码
在咱们的贴纸对象被添加进来后咱们才能够继续接下来的操做,在咱们触摸屏幕时,要判断是否按在贴纸区域,按在哪一个贴纸上。实现比较简单,咱们的每一个Sticker都有一个矩形范围,在通过移动缩放之类的操做后也能够经过Matrix来轻松获得那个矩形区域(Rect
类),只须要判断这个范围是否包含咱们按下的点,而这一步应该在Touch事件的ACTION_DOWN
事件中进行。
switch (action) {
case MotionEvent.ACTION_DOWN:
mCurrentMode = ActionMode.DRAG;
mDownX = event.getX();
mDownY = event.getY();
mHandlingSticker = findHandlingSticker();
……
}复制代码
其中findHandlingSticker()
正是作了这样一些事情
private Sticker findHandlingSticker() {
for (int i = mStickers.size() - 1; i >= 0; i--) {
if (isInStickerArea(mStickers.get(i), mDownX, mDownY)) {
return mStickers.get(i);
}
}
return null;
}复制代码
找到了咱们要操做的Sticker后,咱们就能够对其进行操做了,移动操做最为简单,只涉及一根手指,在ACTION_DOWN
事件中咱们记录下当前Sticker的状态和事件起始坐标,在ACTION_MOVE
事件中,咱们利用当前点的坐标计算出实际偏移量,利用Matrix的postTransition()
方法让Sticker作出随手指的移动。
mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postTranslate(event.getX() - mDownX, event.getY() - mDownY);
mHandlingSticker.getMatrix().set(mMoveMatrix);复制代码
通常的缩放与旋转操做都是须要两根手指,因此咱们须要在ACTION_POINT_DOWN
事件中监听第二根手指按下。这时咱们还须要计算出两根手指之间的距离以及中心点还有角度,由于咱们要让Sticker以这个中心点为中心缩放旋转,在ACTION_MOVE
事件中以新的两指尖距离/起始两指尖距离做为缩放比缩放。以新的角度-起始角度做为旋转角。
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
mOldDistance = calculateDistance(event);
mOldRotation = calculateRotation(event);
mMidPoint = calculateMidPoint(event);
……
}复制代码
相应的缩放与旋转,利用Matrix的postScale
和postRotate
方法实现
float newDistance = calculateDistance(event);
float newRotation = calculateRotation(event);
mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postScale(newDistance / mOldDistance, newDistance / mOldDistance, mMidPoint.x, mMidPoint.y);
mMoveMatrix.postRotate(newRotation - mOldRotation, mMidPoint.x, mMidPoint.y);
mHandlingSticker.getMatrix().set(mMoveMatrix);复制代码
在通过上面的步骤后,咱们的StickerView已经能够添加贴纸,用手势操纵贴纸移动,缩放,旋转了,可是咱们并无对选中的贴纸进行特殊处理,由于通常的应用对于选中的贴纸,都会用一个边框围住,并在相应的边框边角显示一些操做按钮。由于这个按钮有图标,因此咱们也能够把其做为一个Sticker,只是还须要一个位置的x,y值。
public class BitmapStickerIcon extends DrawableSticker {
private float x;
private float y;
……
}复制代码
由于对于每一个Sticker的边框及其坐标是很容易得到的,因此咱们只须要在onDraw
方法中在正在处理的Sticker周围画上边框和按钮就能够了。下面的代码得到了选中Sticker的边角坐标,并将操做按钮画在相应位置。
if (mHandlingSticker != null && !mLooked) {
float[] bitmapPoints = getStickerPoints(mHandlingSticker);
float x1 = bitmapPoints[0];
float y1 = bitmapPoints[1];
float x2 = bitmapPoints[2];
float y2 = bitmapPoints[3];
float x3 = bitmapPoints[4];
float y3 = bitmapPoints[5];
float x4 = bitmapPoints[6];
float y4 = bitmapPoints[7];
canvas.drawLine(x1, y1, x2, y2, mBorderPaint);
canvas.drawLine(x1, y1, x3, y3, mBorderPaint);
canvas.drawLine(x2, y2, x4, y4, mBorderPaint);
canvas.drawLine(x4, y4, x3, y3, mBorderPaint);
float rotation = calculateRotation(x3, y3, x4, y4);
//draw delete icon
canvas.drawCircle(x1, y1, mIconRadius, mBorderPaint);
mDeleteIcon.setX(x1);
mDeleteIcon.setY(y1);
mDeleteIcon.getMatrix().reset();
mDeleteIcon.getMatrix().postRotate(
rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
mDeleteIcon.getMatrix().postTranslate(
x1 - mDeleteIcon.getWidth() / 2, y1 - mDeleteIcon.getHeight() / 2);
mDeleteIcon.draw(canvas);
//draw zoom icon
canvas.drawCircle(x4, y4, mIconRadius, mBorderPaint);
mZoomIcon.setX(x4);
mZoomIcon.setY(y4);
mZoomIcon.getMatrix().reset();
mZoomIcon.getMatrix().postRotate(
45f + rotation, mZoomIcon.getWidth() / 2, mZoomIcon.getHeight() / 2);
mZoomIcon.getMatrix().postTranslate(
x4 - mZoomIcon.getWidth() / 2, y4 - mZoomIcon.getHeight() / 2);
mZoomIcon.draw(canvas);
//draw flip icon
canvas.drawCircle(x2, y2, mIconRadius, mBorderPaint);
mFlipIcon.setX(x2);
mFlipIcon.setY(y2);
mFlipIcon.getMatrix().reset();
mFlipIcon.getMatrix().postRotate(
rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
mFlipIcon.getMatrix().postTranslate(
x2 - mFlipIcon.getWidth() / 2, y2 - mFlipIcon.getHeight() / 2);
mFlipIcon.draw(canvas);
}复制代码
这样,咱们大体完成了StickerView的全部功能,固然上面并无太完整的代码,只是一些代码片断,可是已经说明了大体的思路及操做,想了解更多细节能够去查看源码。咱们在自定义View时,首先最须要的是一个思路,有了思路以后要想其代码结构,在这两块都想好了之后再开发其功能,会事半功倍。
但愿能够对你有帮助。若是有什么疑问,能够随时联系我,欢迎提issue和pr。