效果以及区别
这是两个项目,一个是myView继承自View,一个是myImageView继承自ImageView,myView中的圆形会根据手指移动,即使手指并未点到圆形上。myImageView是一个图片,手指必须点在图片上才能移动图片
区别:myView移动的是自定义View中的
内容,通过View的scrollTo方法实现圆形随手指移动的效果,myImageView通过setFrame方法实现图片随手指移动的效果,移动的是整体
几点说明
这篇博客主要讲myView的实现,
只说项目中用到的知识点,myImageView只说关键点
1、自定义myView是整个粉红色的区域和圆,圆只是myView中的内容
2、scrollTo移动的是View的内容,并不移动View本身,这就意味着如果View本身有个onClick事件,虽然View的内容向右移动了100dp,但是点击View原来的位置还是会触发onClick事件,而显式的位置不会触发,和Android中的传统动画一样
3、因为myView是继承View的所以要自己处理view的LayoutParams为wrap_content的情况以及View的padding属性,而View的margin由父容器处理无需自己处理,如何处理下面会说明,如果myView继承的是系统控件,比如继承自TextView则
不需要自己处理warp_content、padding
实现思路
自定义View的四个步骤:
1、定义View的属性
2、获取View的属性,我是在myView的构造函数中获取的
3、重写onMeasure方法,在这里处理LayoutParams为wrap_content的情况,原因下方会说明
4、重写onDraw方法,用于绘制View,在这里处理View的padding属性
上面的1,2主要是为了修改View的属性时(比如View颜色)不需要改代码只要该xml中的属性就行,提高代码的复用性
1、定义View的属性在项目的res\values中建个xml,我这里取的名字是attrs,代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
2、获取View的属性:
//获取属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = array.getColor(R.styleable.CircleView_circle_color, Color.BLACK);
array.recycle();
3、重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,MeasureSpec有两部分组成
SpecMode
和
SpecSize,其中SpecMode有三种类型
1、UNSPECIFIED:父容器不对View有任何限制,要多大给多大,一般不用
2、EXACTILY:父容器已经检测出View所需要的精确大小,对应LayoutParams中的
match_parent和具体数值,该类型下的SpecSize
是parentSize(父容器剩余的空间)
3、AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,
对应LayoutParams中的wrap_content,该类型下的SpecSize
是parentSize(父容器剩余的空间)
通过上面介绍我们可以知道match_parent和wrap_content是SpecSize是一样的,这就意味着View实现的效果一样,wrap_content作废了,解决办法就是当指定wrap_content时给个默认的大小,
onMeasure代码,mWidth,mHeight为指定的默认大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, mHeight);
} else {
setMeasuredDimension(widthSize, heightSize);
}
}
4、重写onDraw方法,处理View的padding属性
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
/**
* 下面这个是根据布局文件中layout_width和layout_height的数值画出相应的圆
*/
int radius = Math.min(width, height) / 2;
//圆心和半径考虑到View四周的padding,做出相应的调整
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
myView的完整代码:
package com.example.he.circleview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* 继承自View需要自己处理padding和warp_content,margin由父容器处理无需自己处理
* Created by he on 2016/12/14.
*/
public class CircleView extends View {
private static final String TAG = "myView";
//处理warp_content,设置默认值
private int mWidth = 200;
private int mHeight = 400;
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Context mContext;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
//获取属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = array.getColor(R.styleable.CircleView_circle_color, Color.BLACK);
array.recycle();
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, mHeight);
} else {
setMeasuredDimension(widthSize, heightSize);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
/**
* 下面这个是根据布局文件中layout_width和layout_height的数值画出相应的圆
*/
// int radius = Math.min(width, height) / 2;
// //圆心和半径考虑到View四周的padding,做出相应的调整
// canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
/**
* 下面是画出指定大小的圆,为了方便展示View随手指移动
*/
int radius = 50;
//圆心和半径考虑到View四周的padding,做出相应的调整
canvas.drawCircle(50, 50, radius, mPaint);
}
/**
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
/**
* mScrollX是View左边缘的X坐标减去View内容左边缘的X坐标,因此如果手指是从左向右滑,
则mScrollX为负值,反之正值,mScrollY也是一样的,见 scrollTo方法的源码
*/
scrollTo(-x, -y);
}
return true;
}
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.he.circleview.MainActivity">
<com.example.he.circleview.CircleView
android:id="@+id/myCircle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"
android:padding="20dp"
android:onClick="true"
app:circle_color="@color/colorPrimary"
android:layout_margin="10dp"
/>
</RelativeLayout>
注意这里xmlns:app="http://schemas.android.com/apk/res-auto",声明了一个命名空间
(一定要写!!!,在AS中写个app然后连敲回车就行了),用于指定View的属性,
app:circle_color="@color/colorPrimary"这里指定了view中圆形的颜色
myView的源码: 点击打开链接
关于myImageView,代码就是你百度搜“随手指移动的imageView”首页前面几个基本都是这个代码,但是这段代码有个问题,就是即使手指并没有点在image上,image仍然能随手指移动,如果要想实现只有手指在image上才能移动的效果,需要重写myImageView的onTouchEvent()方法,
并返回false,利用了View的事件分发机制
@Override
public boolean onTouchEvent(MotionEvent event) {
press=true;
// autoMouse(event);
return false;
}
activity的onTouchEvrnt方法
@Override
public boolean onTouchEvent(MotionEvent event) {
if (image.isPress())
image.autoMouse(event);
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
image.setPress(false);
break;
}
return false;
}
返回false所表示的含义是当前View不消耗此事件,
当前事件会
向上传递
,但是
在同一个事件序列中,当前View无法再次接收到事件,因为这里只有这一个view没别的了,所以不消耗的事件最终会交给Activity的onTouchEvent()处理,在这里判断手指是否按在image上,如果是则移动
View事件的传递:Activity--->Window--->View
事件序列:从手指按下去开始到手指拿起来结束
关于这部分详细内容请参考《Android开发艺术探索》的第三章