最近研究公司老的图片处理框架,根据新的需求研究出了一套新的图片处理框架,性能上估计没有什么提升,不过整体的代码质量有了非常高的改进,因为毕竟要作为往下传的框架,代码要优雅装逼,可维护性好。这里估计会写两三篇博客讲这个框架,这是第一篇。
本篇博客实现的功能很简单,就是点击添加素材按钮,然后在屏幕上点击出现图片(张学友表情包),可以放大缩小旋转和删除,点击屏幕其他位置就算是取消选中,再点击可以重新选中对其进行操作,再点击按钮也可以再往上面添加图片。如图所示。
整个Activity的上部分就是个自定义ViewGroup,自定义类PaintLayout 继承FrameLayout,因为图片可以在viewgroup随意拖动,所以用FrameLayout比较合适。下方就是个LingearLayout,放几个简单的按钮,这里就一个添加素材的按钮。布局代码:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.administrator.customview.MainActivity">
<com.example.administrator.customview.PaintLayout
android:id="@+id/paint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="40dp">
</com.example.administrator.customview.PaintLayout>
<LinearLayout
android:weightSum="4"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="40dp">
<Button
android:id="@+id/add_btn"
android:text="添加素材"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>
这里既然有了自定义的ViewGroup,那就再自定义View,这也几乎就是本节的重难点,自定义PaintView继承View,在PaintView里面定义一个接口,暴露给外层的ViewGroup去处理删除和选中事件
public interface OnEnableListener{
void callback(PaintView stampView);
void onDelete(PaintView stampView);
}
这里我们可以用枚举去定义状态和触摸点,状态有五种,分别是啥都没,旋转,缩放,移动,删除。触摸点应该是9个,不过这里只要四个就行了,分别是左上,右上,右下和中间
public enum Status{NONE,ROTATE,SCALE,MOVE,DELETE}
/* 图片控制点
* 0---1---2
* | |
* 7 8 3
* | |
* 6---5---4
*/
private enum PositionType{TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT}
public static final int CTR_LEFT_TOP = 0;
public static final int CTR_RIGHT_TOP = 2;
public static final int CTR_RIGHT_BOTTOM = 4;
public static final int CTR_MID_MID = 8;
private Status status;
private PositionType positionType;
这里中间的主图片,旋转图片,删除图片,缩放图片(两张),都需要定义;这里的移动、缩放和旋转都需要矩阵Matrix;屏幕的长宽;图片的长宽;按钮的长宽;是否显示按钮的标志值;画线条和按钮的画笔;还有辅助数据;
private Bitmap deleteBmp,rotateBmp,scaleBmp,scaleBmp2;
private Bitmap bmp;
private Matrix matrix;
private int viewWidth,viewHeight;
private int bmpWidth,bmpHeight;
private int toolBmpWidth,toolBmpHeight;
private Paint paint;
private boolean viewEnable=false;
private RectF srcRect, dstRect,horizontalRect, verticalRect, obliqueRect;
private transient PointF pointHorizontal, pointVertical, pointOblique;
private float[] srcPs, dstPs;
private float preDegree;
private float diagonalLength = 0f;
private float scaleTotal = 1f;
private float deltaX = 0, deltaY = 0;
需要定义的数据就这么多,现在构造方法中构造一些必须的数据
public PaintView(Context context,int pointX,int pointY,OnEnableListener listener) {
super(context);
this.listener=listener;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
viewWidth = wm.getDefaultDisplay().getWidth();
viewHeight = wm.getDefaultDisplay().getHeight();
deleteBmp= BitmapFactory.decodeResource(context.getResources(),R.drawable.delete2);
rotateBmp=BitmapFactory.decodeResource(context.getResources(),R.drawable.rotate);
scaleBmp=BitmapFactory.decodeResource(context.getResources(),R.drawable.scale);
scaleBmp2=BitmapFactory.decodeResource(context.getResources(),R.drawable.scale2);
bmp=BitmapFactory.decodeResource(context.getResources(),R.drawable.a13);
bmpWidth=bmp.getWidth();
bmpHeight=bmp.getHeight();
toolBmpWidth=rotateBmp.getWidth();
toolBmpHeight=rotateBmp.getHeight();
srcPs = new float[]{0, 0, bmpWidth / 2, 0, bmpWidth, 0, bmpWidth,
bmpHeight / 2, bmpWidth, bmpHeight,bmpWidth / 2, bmpHeight,
0, bmpHeight, 0, bmpHeight / 2, bmpWidth / 2, bmpHeight / 2};
dstPs = srcPs.clone();
srcRect = new RectF(0, 0,bmpWidth,bmpHeight);
dstRect = new RectF();
matrix=new Matrix();
matrix.setTranslate(pointX,pointY);
matrix.mapPoints(dstPs, srcPs);
matrix.mapRect(dstRect, srcRect);
paint = new Paint();
paint.setStrokeWidth(5);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setPathEffect(new DashPathEffect(new float[]{10, 10}, 1));
lastPoint = new Point(0, 0);
prePoint = new Point(0, 0);
}
接下来是实现如何在放大缩小以及旋转时控制几个按钮的位置,直接上代码
private void drawButton(Canvas canvas){
float extendLength = getExtendLength();
positionType= getPositionType();
pointHorizontal = getHorizontalPoint(positionType, dstRect, extendLength);
pointVertical = getVerticalPoint(positionType, dstRect, extendLength);
pointOblique = getObliquePoint();
canvas.drawLine(dstPs[CTR_MID_MID*2], dstPs[CTR_MID_MID*2+1], pointHorizontal.x, pointHorizontal.y, paint);
canvas.drawLine(dstPs[CTR_MID_MID*2], dstPs[CTR_MID_MID*2+1], pointVertical.x, pointVertical.y, paint);
canvas.drawLine(dstPs[CTR_MID_MID * 2], dstPs[CTR_MID_MID * 2 + 1], pointOblique.x, pointOblique.y, paint);
canvas.drawBitmap(deleteBmp, pointVertical.x - deleteBmp.getWidth() / 2
, pointVertical.y - deleteBmp.getHeight() / 2, null);
canvas.drawBitmap(rotateBmp, pointHorizontal.x - scaleBmp.getWidth() / 2
, pointHorizontal.y - scaleBmp.getHeight() / 2, null);
if(positionType== PositionType.TOP_LEFT||positionType== PositionType.BOTTOM_RIGHT){
canvas.drawBitmap(scaleBmp2, pointOblique.x-rotateBmp.getWidth()/2, pointOblique.y-rotateBmp.getHeight()/2,null);
}else {
canvas.drawBitmap(scaleBmp, pointOblique.x-rotateBmp.getWidth()/2, pointOblique.y-rotateBmp.getHeight()/2,null);
}
float expend_width = 20f;
horizontalRect = new RectF(pointHorizontal.x - toolBmpWidth / 2- expend_width,
pointHorizontal.y - toolBmpHeight / 2 - expend_width,
pointHorizontal.x + toolBmpWidth / 2 + expend_width,
pointHorizontal.y + toolBmpHeight / 2 + expend_width);
verticalRect = new RectF(pointVertical.x - toolBmpWidth / 2- expend_width,
pointVertical.y - toolBmpHeight/ 2 - expend_width,
pointVertical.x + toolBmpWidth / 2 + expend_width,
pointVertical.y + toolBmpHeight/ 2 + expend_width);
obliqueRect = new RectF(pointOblique.x - toolBmpWidth / 2- expend_width,
pointOblique.y - toolBmpHeight / 2 - expend_width,
pointOblique.x + toolBmpWidth / 2 + expend_width,
pointOblique.y + toolBmpHeight / 2 + expend_width);
}
private PositionType getPositionType(){
if (dstPs[CTR_MID_MID*2] < (viewWidth / 2)){
if (dstPs[CTR_MID_MID*2+1] < (viewHeight / 2)){
return PositionType.TOP_LEFT;
} else {
return PositionType.BOTTOM_LEFT;
}
} else {
if (dstPs[CTR_MID_MID*2+1] < (viewHeight / 2)){
return PositionType.TOP_RIGHT;
} else {
return PositionType.BOTTOM_RIGHT;
}
}
}
private PointF getHorizontalPoint(PositionType positionType, RectF rectF, float extendLength){
PointF hPoint = new PointF();
switch (positionType){
case TOP_LEFT:
case BOTTOM_LEFT:
hPoint.x = rectF.right + extendLength;
break;
case TOP_RIGHT:
case BOTTOM_RIGHT:
hPoint.x = rectF.left - extendLength;
break;
default:
break;
}
if (dstPs[CTR_MID_MID*2+1] < 20f){
hPoint.y = 20f;
} else if (dstPs[CTR_MID_MID*2+1] > (viewHeight - 20f)) {
hPoint.y = viewHeight - 20f;
} else {
hPoint.y = dstPs[CTR_MID_MID*2+1];
}
return hPoint;
}
private PointF getVerticalPoint(PositionType positionType, RectF rectF, float extendLength){
PointF hPoint = new PointF();
switch (positionType){
case TOP_LEFT:
case TOP_RIGHT:
hPoint.y = rectF.bottom + extendLength;
break;
case BOTTOM_LEFT:
case BOTTOM_RIGHT:
hPoint.y = rectF.top - extendLength;
break;
default:
break;
}
if (dstPs[CTR_MID_MID*2] < 20f){
hPoint.x = 20f;
} else if (dstPs[CTR_MID_MID*2] > (viewWidth - 20f)) {
hPoint.x = viewWidth - 20f;
} else {
hPoint.x = dstPs[CTR_MID_MID*2];
}
return hPoint;
}
private PointF getObliquePoint(){
PointF oPoint = new PointF();
oPoint.x= pointHorizontal.x;
oPoint.y=pointVertical.y;
return oPoint;
}
private float getExtendLength(){
float max_length = 180f;
float min_length = 40f;
float length = diagonalLength * scaleTotal;
if (length < (max_length - min_length)){
return max_length - length;
} else {
return min_length;
}
}
这段代码通过计算位置和缩放控制几个按钮的摆放
再通过触摸点的位置坐标去计算究竟触摸了哪一个按钮
private Status getCurrentStatue(float x,float y) {
if (viewEnable) {
//Respond area
if (horizontalRect != null && horizontalRect.contains(x, y)) {
//rotate
return Status.ROTATE;
} else if (verticalRect != null && verticalRect.contains(x, y)) {
//delete
return Status.DELETE;
} else if (obliqueRect != null && obliqueRect.contains(x, y)) {
//scale
return Status.SCALE;
}
}
float a1 = Math.abs(dstPs[CTR_LEFT_TOP * 2] - dstPs[CTR_RIGHT_TOP * 2]);
float b1 = Math.abs(dstPs[CTR_LEFT_TOP * 2 + 1] - dstPs[CTR_RIGHT_TOP * 2 + 1]);
int realWidth = (int) Math.sqrt(a1 * a1 + b1 * b1);
float a2 = Math.abs(dstPs[CTR_RIGHT_TOP * 2] - dstPs[CTR_RIGHT_BOTTOM * 2]);
float b2 = Math.abs(dstPs[CTR_RIGHT_TOP * 2 + 1] - dstPs[CTR_RIGHT_BOTTOM * 2 + 1]);
int realHeight = (int) Math.sqrt(a2 * a2 + b2 * b2);
RectF rectf1 = new RectF(dstPs[CTR_MID_MID * 2] - realWidth / 2, dstPs[CTR_MID_MID * 2 + 1] - realHeight / 2
, dstPs[CTR_MID_MID * 2] + realWidth / 2, dstPs[CTR_MID_MID * 2 + 1] + realHeight / 2);
if (rectf1.contains(x, y)) {
return Status.MOVE;
}
return Status.NONE;
}
封装相应的旋转,缩放,和移动函数
private void translate(){
deltaX = (prePoint.x - lastPoint.x);
deltaY = (prePoint.y - lastPoint.y);
matrix.postTranslate(deltaX, deltaY);
reMapping();
}
private void rotate() {
if (Math.abs(prePoint.x - lastPoint.x) > 5 || Math.abs(prePoint.y - lastPoint.y) > 5) {
preDegree = computeDegree(prePoint, new Point((int) dstPs[2], (int) dstPs[3]))
- computeDegree(lastPoint, new Point((int) dstPs[2], (int) dstPs[3]));
matrix.postRotate(preDegree, dstPs[CTR_MID_MID * 2], dstPs[CTR_MID_MID * 2 + 1]);
reMapping();
}
}
private void scale(){
//计算缩放触摸点到图片中点的距离
float a,b,diagonalL1;
a=dstPs[CTR_MID_MID*2]-lastPoint.x;
b=dstPs[CTR_MID_MID*2+1]-lastPoint.y;
diagonalL1= (float) Math.sqrt(a*a+b*b);
//计算触摸前后两个点的偏移
float c,d,diagonalL2;
c=dstPs[CTR_MID_MID*2]-prePoint.x;
d=dstPs[CTR_MID_MID*2+1]-prePoint.y;
diagonalL2= (float) Math.sqrt(c*c+d*d);
float scaleValue=diagonalL2/diagonalL1;
matrix.postScale(scaleValue,scaleValue,dstPs[CTR_MID_MID*2],dstPs[CTR_MID_MID*2+1]);
reMapping();
}
private float computeDegree(Point p1, Point p2) {
float tran_x = p1.x - p2.x;
float tran_y = p1.y - p2.y;
float degree = 0.0f;
float angle = (float) (Math.asin(tran_x / Math.sqrt(tran_x * tran_x + tran_y * tran_y)) * 180 / Math.PI);
if (!Float.isNaN(angle)) {
if (tran_x >= 0 && tran_y <= 0) {//第一象限
degree = angle;
} else if (tran_x <= 0 && tran_y <= 0) {//第二象限
degree = angle;
} else if (tran_x <= 0 && tran_y >= 0) {//第三象限
degree = -180 - angle;
} else if (tran_x >= 0 && tran_y >= 0) {//第四象限
degree = 180 - angle;
}
}
return degree;
}
private void reMapping(){
matrix.mapPoints(dstPs, srcPs);
matrix.mapRect(dstRect, srcRect);
invalidate();//重绘
}
重写onDraw和onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
status=getCurrentStatue((int) event.getX(), (int) event.getY());
if(status!= Status.NONE){
lastPoint.set((int)event.getX(),(int)event.getY());
viewEnable=true;
if(status== Status.DELETE){
listener.onDelete(this);
}else {
setViewEnable(true);
listener.callback(this);
}
} else {
viewEnable=false;
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
prePoint.set((int)event.getX(),(int)event.getY());
if(status== Status.MOVE){
translate();
}else if(status== Status.ROTATE){
rotate();
}else if(status== Status.SCALE){
scale();
}
lastPoint.set(prePoint.x,prePoint.y);
break;
case MotionEvent.ACTION_UP:
status= Status.NONE;
break;
}
return status== Status.NONE?false:true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(viewEnable){
drawButton(canvas);
}
canvas.drawBitmap(bmp,matrix,null);
}
这里就将图片的所有动作全部封装在了自定义View的内部,只需要暴露给外层些许接口实现一些功能就可以了,再写我们的自定义ViewGroup,很简单,就直接上全部代码了
public class PaintLayout extends FrameLayout implements PaintView.OnEnableListener{
private boolean canInit;
private Context context;
private LayoutParams params;
public PaintLayout(Context context) {
this(context,null);
}
public PaintLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.context=context;
params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
private void addView(MotionEvent event){
PaintView paintView=new PaintView(context,(int)event.getX(),(int)event.getY(),this);
this.addView(paintView,params);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(canInit&&event.getAction()==MotionEvent.ACTION_DOWN){
addView(event);
canInit=false;
}
return super.onTouchEvent(event);
}
@Override
public void callback(PaintView stampView) {
for (int i = 0; i < this.getChildCount(); ++i) {
if (this.getChildAt(i) != stampView) {
((PaintView) this.getChildAt(i)).setViewEnable(false);
}
}
}
@Override
public void onDelete(PaintView stampView) {
stampView.setVisibility(GONE);
}
public void setCanInit(boolean canInit) {
this.canInit = canInit;
}
}
这样就完成了一些基本功能,不过需求不仅仅如此,所以未完待续。