一个随手指移动的自定义View和一个随手指移动的ImageView(附源码)

效果以及区别

这是两个项目,一个是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开发艺术探索》的第三章

myImageView的源码点击打开链接







猜你喜欢

转载自blog.csdn.net/fuckluy/article/details/53671872