四种常用的标准自定义View方法(上)

          感谢巨人的肩膀-------coder任玉刚+Tomcat的猫

(一)继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

先写一个标准的菜鸟级别的自定义View:

CircleView.java

public class CircleView extends View {

    private int mColor = Color.GREEN;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height)/2;
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);

    }

}

代码逻辑非常简单,就不在赘述了;

activity_main.xml :

<com.example.coustomview.CircleView
        android:id="@+id/my_circleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000"/>

运行结果像这样:

然后,向activity_main.xml中加入margin边距,像下面这样:

  <com.example.coustomview.CircleView
        android:id="@+id/my_circleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"/>

运行结果:

可以看到margin已经生效,为什么呢,因为magin是由父控件来控制的,不懂的搜下,很简单,咱们继续,向xml中加入padding后,如下:

  <com.example.coustomview.CircleView
        android:id="@+id/my_circleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000"/>

运行结果:

padding表示内边距,即圆形与外围的矩形间应该有20dp的距离,现在你没有看错,它居然还是内切圆!说明我们的padding属性已经失效了~23333 , 可是这是为什么呢?这就要从我们的宽高说起了,应为我们定义的属性一个是match_parent ,一个是100dp ,so执行的是精确测量模式,默认是不对padding做处理的,so可怜的padding君就被废弃了,至于如何自定义View?方法及其步骤,自己百度下就ok了,这里就不在赘述了,咱们废话不多说,那么如何解决呢?既然,OnMeasure( )这个渣渣不给咱做处理,那咱就自己弄吧~走起!

试着把android:layout_width="match_parent" 的属性值改为android:layout_width="wrap_content" 你会发现有卵用,运行结果并没有任何改变,原因是在自定义的View(特指直接继承自View的类)中,你如果不对wrap_content做特殊处理,它就跟match_parent没什么区别了,so效果就是一样的,至于原因自己百度下,这里就不在赘述了,那么怎么让padding生效呢?很简单只需要在onDraw()中做处理就ok了,就像这样:

@Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        //对padding做特殊处理
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int PaddingButtom = getPaddingBottom();

        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - PaddingButtom;
        int radius = Math.min(width, height)/2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);

    }

运行结果:

响应的圆心跟半径也做了响应的处理,不懂的画画图,带入几个数字,画个数轴,很简单;

很多时候,我们希望加入自己的自定义属性,可以这样,在values目录下新建attrs.xml(名字而已随意),当然为了规范最好是attrs_XXX这样就很明显了,对吧~~attrs.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>

</resources>

然后我们加入如下代码:

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //加载自定义属性集合CircleView  

TypedArray taArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);  

        //解析CircleView属性结合中的属性circle_text 跟 text_size;
        mColor = taArray.getColor(R.styleable.CircleView_circle_color, Color.GREEN);
        taArray.recycle();
        init();
    }

就可以看到,我们自定的属性生效了,哇哈哈~完整代码如下:

package com.example.coustomview;

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.util.TypedValue;
import android.view.View;
/**
 * 继承View重写onDraw方法
 * 这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,
 * 采用这种方式需要自己支持wrap_content,并且padding也需要
 * 自己处理。
 *
 * @author Eillot
 *
 */
public class CircleView extends View {

    private int mColor = Color.GREEN;
    private Paint  mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); ;

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //加载自定义属性集合CircleView
        TypedArray taArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        //解析CircleView属性结合中的属性circle_text 跟 text_size;
        mColor = taArray.getColor(R.styleable.CircleView_circle_color, Color.GREEN);
        taArray.recycle();
        init();
    }

    private void init() {

        mPaint.setColor(mColor);

    }

    /**
     * 在自定义view中需要支持宽高属性为wrap_content的情况,则需要重写View测量方法OnMeasure();
     *
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightSpaceMode);

        if (widthSpaceMode == MeasureSpec.AT_MOST && heightSpaceMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        }else if (widthSpaceMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpaceSize);
        }else if ( heightSpaceMode == MeasureSpec.AT_MOST)
        {
            setMeasuredDimension(widthSpaceSize, 200);
        }

    }
    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        //对padding做特殊处理
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int PaddingButtom = getPaddingBottom();

        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - PaddingButtom;
        int radius = Math.min(width, height)/2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }

}

activity_main.xml中的代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:coustom="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical"
    tools:context="com.example.coustomview.MainActivity" >

    <eliot.wakfo.com.coustomview2.CircleView
        android:id="@+id/my_circleView"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        coustom:circle_color="@color/circle_color"
        android:padding="20dp"
        android:background="#000000"/>

</LinearLayout>

运行结果如下:

简直就是DXX了,高级新技能已经get !继续~~

(二)继承ViewGroup派生特殊的Layout

这是真真实实的造”轮子“呀!需要你自己写view的OnMeasure()跟OnLayout()过程的逻辑,如果想写一个listView+Scrollview的变异Layout,那你还要处理滑动冲突的问题,我原来一直不明白,为什么我一定要懂事件分发机制(内部是一个树形结构),它有什么用?我现在知道了,那就是几乎所有的自定义相关的View或ViewGroup它在写处理的逻辑的时候的基础就是事件分发,比如:自定义Draw(),它内部有一个dispatchDraw()方法,一看名字是不是非常熟悉,哈哈~没错,跟Event的分发很类似,然后,我又想到——-为什么我要学习《离散数学》了?如果当时老师告诉我学它是干嘛用的,我一定好好学习,学渣表示已经还给老师了~好了,我们废话不多说,走起!

我们先来看自定义ViewGroup的OnMeasure()方法(为什么先从它开始,因为这个渣渣很容易出错,而且很关键);

思路:

以前我们自定义View的时候,我只需要考虑它自己的测量就可以了,现在ViewGroup中放了很多个View ,你说怎么测量?当然是把它(即遍历)出来,然后测出宽高,然后求和,就是我要画的总宽高了呀!当然,这里有个假设必须成立—–那就是子View的宽高均相等;结合具体情况具体使用呀~比如:我要定义一个类似水平的LinearLayout ,这时候我的高就等于第一个子View的高,而宽则等于所以子View的总和,why ?因为你看手机图片的时候是不是左右滑动,而不是上下滑动,对否~上代码:

  创建CoustomViewGroup.java
 /**
     * view测量原理:
     *      主要是MeasureSpace代表一个int 32位的值,高俩位分别为spaceMode(即测量模式),spaceSize(即测量大小)
     *      那3中测试模式这里就赘述了,自己搜一下,主要针对说下宽高属性为wrap_content的情况,加入任一属性为wrap_content时,
     *      高(宽)需要在onMeasure()方法中做特殊处理,不复杂,就是给一个默认值比如:200dp ,我在网上看到很多人都喜欢使用这个数字,
     *      不知道为什么?若二者都为wrap_content ,简单那就在OnMeasure()中都做处理给个默认值呗,就这么简单!
     *
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //定义俩个保存子Viw的测量宽跟高的变量measureWidth和measureHeight
        int measureChildWidth = 0;
        int measureChildheight = 0;
        //获取子view的个数
        final int childCount = getChildCount();
        //测量ziView的宽高;
        measure(widthMeasureSpec , heightMeasureSpec);

        //下来就是套路了,确定测量的模式跟大小

        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int widSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);

        //下来就是写自己的逻辑判断了,这里必须感谢下大牛----coder任玉刚 ,解决了我许久的困惑,
        // 比如:为什么自定义view时宽高设置为warp_content时,不重写OnMeasure()方法,View的效果等同于math_parent?

        //先来判断下有没有子元素,没有就不用测了直接置0
        if (childCount == 0){
            setMeasuredDimension(0,0);
        }else if ( (widthSpaceMode == MeasureSpec.AT_MOST) && (heightSpaceMode == MeasureSpec.AT_MOST) ){

            //还记得我们自定义View的时候的处理规范吗------setMeasuredDimension(200 , 200);

            //获取第一个子View的对象
            final View childView = getChildAt(0);
            measureChildWidth = childView.getMeasuredWidth() * childCount;
            measureChildheight = childView.getMeasuredHeight() * childCount;
            setMeasuredDimension(measureChildWidth , measureChildheight);

        }else if ( widthSpaceMode == MeasureSpec.AT_MOST ){

            //当宽属性为wrap_content时,需要所有子View的宽之和(记得我们是水平的呀)
            final View childView = getChildAt(0);
            measureChildWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureChildWidth , heightSpaceSize);

        }else  if ( heightMeasureSpec == MeasureSpec.AT_MOST ){

            //注意:当高属性为wrap_content时,仅仅需要任一子View的高即可(记得我们是左右滑动的,高是不变得)
            final View childView = getChildAt(0);
            measureChildheight = childView.getMeasuredHeight() ;
            setMeasuredDimension(widSpaceSize , measureChildheight);
        }
    }

代码中的注释已经很完整了,这里就不在解释代码了~~我们继续,

NPC “任教主”的温馨提示:

上面的OnMeasure()方法有俩点不规范:

NO .1 当没有子元素的时候,不应该直接把宽高置为0 ,而应该根据LayoutParams中的宽高来做相应的处理;

No.2 在测量CoustomViewGroup的时候没有考虑它的padding 跟子View的margin会影响到CoustomViewGroup的宽高,why ?因为不管是自己的padding或者是子View的margin占用的都是CoustomViewGroup的空间;

小伙伴们,可以自己实现下,我们主要学习流程就不走细节了,咱们继续,来看看自定义ViewGroup的OnLayout()的过程,走起!

/**
     * onLayout()过程主要用于确定view在ViewGroup中的摆放位置,通过确定View的L , T,R ,B四个坐标点;
     * @param b
     * @param i
     * @param i1
     * @param i2
     * @param i3
     */
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

        int childLeft = 0;
        final int childCount = getChildCount();
        //mChildrenSize = childCount;// 这里没搞懂为什么要把childCount赋值给mChildrenSize ?

        //接下来就是你熟悉的套路了,遍历每个子View并获取它们的位置, 从左向右
        for ( int n = 0 ; n < childCount ; n++){
            final  View childView = getChildAt(n);
            //View可见
            if ( (childView.getVisibility()) != View.GONE){
                    final int childWidth = childView.getMeasuredWidth();
                    mChildWidth = childWidth;
                    childView.layout(childLeft , 0 , childLeft + childWidth , childView.getMeasuredHeight());
                    childLeft += childWidth;
            }
        }
    }

同样任教主温馨提示时间到:

No.2 在测量CoustomViewGroup的时候没有考虑它的padding 跟子View的margin会影响到CoustomViewGroup的宽高,why ?因为不管是自己的padding或者是子View的margin占用的都是CoustomViewGroup的空间;

至于怎么处理让它变得规范,即具有处理padding及margin 的能力,就看法宝 吧~下面咱们继续分析:

我们都知道有View的监听方法有一个叫OnTechEvent()的,它里面有3个Action 分别为Action_Down , Action_Move ,ACtion_Up ,那么,我们从左向右滑动图片的时候,必然会涉及到这3个“怪”,我们要做的就是加入自己的逻辑判断规则,来劝化他们,怎么劝化呢?

我们可以这样:

1)手指落下后会触发—-Down , 滑动会触发—-Move ,手指收起会触发—-Up ;

2) 1怪跟3怪不足为虑,主要是2怪,它会产生一个滑动事件,我想让它把这个事件交给onTechEvent()的来处理,好那就重写onTechEvent()然而并没有什么卵用,这就涉及到一个事件分发的问题,至于事件分发的机制,网上多如牛毛,自己搜下吧~我们切入正题,事件分发机制遵循—–谁拦截谁负责到底的原则,当然,前提是onTechEvent()返回True (表示这个事件我来处理了) ,onInterceptTouchEvent()返回Treue(表示事件被截断,不在传递); 那么方法就是重写ViewGroup的onInterceptTouchEvent()方法,加入自己的逻辑判断;就下这样:

/**
     * 为什么要重写ViewGroup的事件分发呢?
     * 若不重写onInterceptTouchEvent(),你会发现即使你的OnTechEvent()方法返回的是True(即应该处理事件),
     * 然而,它却并没有处理事件,why ? 因为事件已经被拦截了!所以,要加入自己的逻辑判断,让onInterceptTouchEvent()
     * 知道什么时候进行拦截,什么时候不进行拦截~~
     *
     * @param event
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                //手指落下时若View平滑滚动还未完成,则打断动画,并对Down事件进行拦截
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }

            case MotionEvent.ACTION_MOVE:{
                intercepted = false;
                int deltaX = x - mLastInterceptX;//View的滑动后距离的X坐标
                int deltaY = y - mLastInterceptY;//View的滑动后距离的Y坐标

                //用于判断是否正在由左向右进行滑动,若是则intercepted = true对事件进行拦截;
                if ( (Math.abs( deltaX)) > (Math.abs(deltaY)) ){

                    intercepted = true;
                }else {

                    intercepted = false;
                }

                break;
            }

            case MotionEvent.ACTION_UP:{

                intercepted = false;
                break;
            }

            default:
                break;

        }

        Log.d( TAG , "intercepted=  " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastInterceptX = x;
        mLastInterceptY = y;

        //不在使用supper来继续使用父类的拦截方法
        return intercepted;
    }

好了,已经成功劝化这3怪,接下来通过使用OnTechEvent()方法来让他们记住我的指令,比如:我发出ACTION_UP的指令,它马上知道,我的手指离开屏幕了,不需要对事件进行处理了,好了,重写OnTechEvent( )方法如下:

 /**
     *  重写onTouchEvent
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mVelocityTracker.addMovement(event);//表示追踪当前点击事件的速度;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN: {
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }

            case MotionEvent.ACTION_UP:{
                int scrollX = getScrollX();
                /**
                 * 表示计算速度,比如:时间间隔为1000 ms ,在1秒内,
                 * 手指在水平方向从左向右滑过100像素,那么水平速度就是100;
                 * 计算速度+获取速度----三步曲
                 * mVelocityTracker.computeCurrentVelocity(1000);
                 float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
                 * float yVelocity = mVelocityTracker.getYVelocity();//获取垂直方向的滑动速度
                 * 由于我们需要的是xVelocity,
                 * 这里只是提一下,不计入代码;
                 * 注意:这里的速度指的是一段时间内手指所滑过的像素数!像素数!像素数!重要事说3遍;
                 */

                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
                /**
                 *当你滑动手机相册中的照片的时候有没有发现,必须滑动到一定距离它才会切到下张图片,
                 * 否则,它就回退回原来的照片了,原来,它是通过“速度”来进行控制的~
                 * 还有就是"速度“可以为负值,很好理解,就像我们规定车前进的方向为正,反向为负;
                 *
                 */

                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;

                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildSize - 1));
                int dx = mChildIndex * mChildIndex - scrollX;//缓慢地滑动到目标的x坐标;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();//对速度跟踪进行回收
                break;
            }

            default: {
                break;
            }
        }

        mLastX  = x ;
        mLastY  = y ;
        return true;
    }

代码中的关键部分我已经给出了注释,这里就不做代码解析了~~

好了,至此我们就完成了“造轮子”的90%的工作,剩下的就是一些优化跟善后工作了,使用Scroller使用我们的滑动看起来更加平滑,然后把速度跟踪,在View的绘制结束后,在onDetachedFromWindow中进行回收,可能大家不知道Scroller的作用,这里简单提一下:

它的本质就是一个View不断重新绘制的过程,直到没有View需要绘制为止,很好理解,就好比是,把一个人的每一个连续动作画下来后,然后叠起来,快速翻页,是不是好像那个人“动”起来了,哇哈哈~~更多详细内容自己搜下,网上教程很多,好了,完整代码如下:



import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * Created by Eillot on 2016/7/12.
 */
public class CoustomViewGroup extends ViewGroup{

    private static final String TAG = "CoustomViewGroup";

    private int mChildSize;
    private int mChildWidth;
    private int mChildIndex;

    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;

    //分别记录上次滑动的坐标对于(onInterceptTouchEvent)

    private int mLastInterceptX = 0;
    private int mLastInterceptY = 0;

    //Scroller类可以让View平滑滚动的一个Helper类;
    private Scroller mScroller;

    //VelocityTracker主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率;
    private VelocityTracker mVelocityTracker;

    public CoustomViewGroup(Context context) {
        super(context);
        init();
    }

    public CoustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CoustomViewGroup(Context context, AttributeSet attrs,
                                  int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {

        if ( mScroller == null){
            mScroller = new Scroller(getContext());
            //获取滑动速率对象
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    /**
     * 为什么要重写ViewGroup的事件分发呢?
     * 若不重写onInterceptTouchEvent(),你会发现即使你的OnTechEvent()方法返回的是True(即应该处理事件),
     * 然而,它却并没有处理事件,why ? 因为事件已经被拦截了!所以,要加入自己的逻辑判断,让onInterceptTouchEvent()
     * 知道什么时候进行拦截,什么时候不进行拦截~~
     *
     * @param event
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                //手指落下时若View平滑滚动还未完成,则打断动画,并对Down事件进行拦截
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }

            case MotionEvent.ACTION_MOVE:{
                intercepted = false;
                int deltaX = x - mLastInterceptX;//View的滑动后距离的X坐标
                int deltaY = y - mLastInterceptY;//View的滑动后距离的Y坐标

                //用于判断是否正在由左向右进行滑动,若是则intercepted = true对事件进行拦截;
                if ( (Math.abs( deltaX)) > (Math.abs(deltaY)) ){

                    intercepted = true;
                }else {

                    intercepted = false;
                }

                break;
            }

            case MotionEvent.ACTION_UP:{

                intercepted = false;
                break;
            }

            default:
                break;

        }

        Log.d( TAG , "intercepted=  " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastInterceptX = x;
        mLastInterceptY = y;

        //不在使用supper来继续使用父类的拦截方法
        return intercepted;
    }

    /**
     *  重写onTouchEvent
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mVelocityTracker.addMovement(event);//表示追踪当前点击事件的速度;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN: {
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }

            case MotionEvent.ACTION_UP:{
                int scrollX = getScrollX();
                /**
                 * 表示计算速度,比如:时间间隔为1000 ms ,在1秒内,
                 * 手指在水平方向从左向右滑过100像素,那么水平速度就是100;
                 * 计算速度+获取速度----三步曲
                 * mVelocityTracker.computeCurrentVelocity(1000);
                 float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
                 * float yVelocity = mVelocityTracker.getYVelocity();//获取垂直方向的滑动速度
                 * 由于我们需要的是xVelocity,
                 * 这里只是提一下,不计入代码;
                 * 注意:这里的速度指的是一段时间内手指所滑过的像素数!像素数!像素数!重要事说3遍;
                 */

                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
                /**
                 *当你滑动手机相册中的照片的时候有没有发现,必须滑动到一定距离它才会切到下张图片,
                 * 否则,它就回退回原来的照片了,原来,它是通过“速度”来进行控制的~
                 * 还有就是"速度“可以为负值,很好理解,就像我们规定车前进的方向为正,反向为负;
                 *
                 */

                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;

                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildSize - 1));
                int dx = mChildIndex * mChildIndex - scrollX;//缓慢地滑动到目标的x坐标;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();//对速度跟踪进行回收
                break;
            }

            default: {
                break;
            }
        }

        mLastX  = x ;
        mLastY  = y ;
        return true;
    }

    /**
     * view测量原理:
     *      主要是MeasureSpace代表一个int 32位的值,高俩位分别为spaceMode(即测量模式),spaceSize(即测量大小)
     *      那3中测试模式这里就赘述了,自己搜一下,主要针对说下宽高属性为wrap_content的情况,加入任一属性为wrap_content时,
     *      高(宽)需要在onMeasure()方法中做特殊处理,不复杂,就是给一个默认值比如:200dp ,我在网上看到很多人都喜欢使用这个数字,
     *      不知道为什么?若二者都为wrap_content ,简单那就在OnMeasure()中都做处理给个默认值呗,就这么简单!
     *
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //定义俩个保存子Viw的测量宽跟高的变量measureWidth和measureHeight
        int measureChildWidth = 0;
        int measureChildheight = 0;
        //获取子view的个数
        final int childCount = getChildCount();
        //测量ziView的宽高;
        measure(widthMeasureSpec , heightMeasureSpec);

        //下来就是套路了,确定测量的模式跟大小

        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int widSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);

        //下来就是写自己的逻辑判断了,这里必须感谢下大牛----coder任玉刚 ,解决了我许久的困惑,
        // 比如:为什么自定义view时宽高设置为warp_content时,不重写OnMeasure()方法,View的效果等同于math_parent?

        //先来判断下有没有子元素,没有就不用测了直接置0
        if (childCount == 0){
            setMeasuredDimension(0,0);
        }else if ( (widthSpaceMode == MeasureSpec.AT_MOST) && (heightSpaceMode == MeasureSpec.AT_MOST) ){

            //还记得我们自定义View的时候的处理规范吗------setMeasuredDimension(200 , 200);

            //获取第一个子View的对象
            final View childView = getChildAt(0);
            measureChildWidth = childView.getMeasuredWidth() * childCount;
            measureChildheight = childView.getMeasuredHeight() * childCount;
            setMeasuredDimension(measureChildWidth , measureChildheight);

        }else if ( widthSpaceMode == MeasureSpec.AT_MOST ){

            //当宽属性为wrap_content时,需要所有子View的宽之和(记得我们是水平的呀)
            final View childView = getChildAt(0);
            measureChildWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureChildWidth , heightSpaceSize);

        }else  if ( heightMeasureSpec == MeasureSpec.AT_MOST ){

            //注意:当高属性为wrap_content时,仅仅需要任一子View的高即可(记得我们是左右滑动的,高是不变得)
            final View childView = getChildAt(0);
            measureChildheight = childView.getMeasuredHeight() ;
            setMeasuredDimension(widSpaceSize , measureChildheight);
        }
    }

    /**
     * onLayout()过程主要用于确定view在ViewGroup中的摆放位置,通过确定View的L , T,R ,B四个坐标点;
     * @param b
     * @param i
     * @param i1
     * @param i2
     * @param i3
     */
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

        int childLeft = 0;
        final int childCount = getChildCount();
        //mChildrenSize = childCount;// 这里没搞懂为什么要把childCount赋值给mChildrenSize ?

        //接下来就是你熟悉的套路了,遍历每个子View并获取它们的位置, 从左向右
        for ( int n = 0 ; n < childCount ; n++){
            final  View childView = getChildAt(n);
            //View可见
            if ( (childView.getVisibility()) != View.GONE){
                    final int childWidth = childView.getMeasuredWidth();
                   mChildWidth = childWidth;
                    childView.layout(childLeft , 0 , childLeft + childWidth , childView.getMeasuredHeight());
                    childLeft += childWidth;
            }
        }
    }

    /**
     * 缓慢滑动到自定位置
     */
    private void smoothScrollBy(int dx, int dy){

        //500ms内滑向dx , 效果就是慢慢地滑动
        mScroller.startScroll(getScrollX() , 0 , dx , 0 , 500 );
        invalidate();
    }

    /**
     * 用于计算出当前滑动的X,Y坐标,即ScrollX ,跟 ScrollY
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()){
            scrollTo( mScroller.getCurrX() , mScroller.getCurrY() );
            postInvalidate();
        }
    }

    /**
     * api原话:
     *将视图从窗体上分离的时候调用该方法。这时视图已经不具有可绘制部分。
     * 即我们已经没有需要绘制的View ,可以回收资源了;
     * 很好理解,你画完图了是不是会保存,然后退出软件;
     *
     */
    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

代码注释已经写了,这里就不在赘述了~activity_main.xml代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical" >

    <com.android.example.CoustomViewGroup
        android:id="@+id/my_CoustomViewGroup"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />

</LinearLayout>

运行结果如下:

目测什么都没有呀~哈哈,你可以加入自己自定的属性,比如:text文本等,就是那个attrs.xml,然后使用TypeArray加载自定义属性并解析赋值的套路呀!自己试试~~

下一节我们说说,继承特定的View,比如TextView ,继承特定的ViewGroup,比如:LinearLayout

时间: 2024-10-10 16:03:04

四种常用的标准自定义View方法(上)的相关文章

Android按钮单击事件的四种常用写法总结

很多学习Android程序设计的人都会发现每个人对代码的写法都有不同的偏好,比较明显的就是对控件响应事件的写法的不同.因此本文就把这些写法总结一下,比较下各种写法的优劣,希望对大家灵活地选择编码方式可以有一定的参考借鉴价值. xml文件代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 <Button     android:id="@+id/button1"     android:layout_width="wrap_content"     

【转】 FPGA设计的四种常用思想与技巧

本文讨论的四种常用FPGA/CPLD设计思想与技巧:乒乓操作.串并转换.流水线操作.数据接口同步化,都是FPGA/CPLD逻辑设计的内在规律的体现,合理地采用这些设计思想能在FPGA/CPLD设计工作种取得事半功倍的效果. FPGA/CPLD的设计思想与技巧是一个非常大的话题,由于篇幅所限,本文仅介绍一些常用的设计思想与技巧,包括乒乓球操作.串并转换.流水线操作和数据接口的同步方法.希望本文能引起工程师们的注意,如果能有意识地利用这些原则指导日后的设计工作,将取得事半功倍的效果! 乒乓操作“乒乓

Android按钮单击事件的四种常用写法

很多学习Android程序设计的人都会发现每个人对代码的写法都有不同的偏好,比较明显的就是对控件响应事件的写法的不同.因此本文就把这些写法总结一下,比较下各种写法的优劣,希望对大家灵活地选择编码方式可以有一定的参考借鉴价值. 四种方法分述如下: 匿名内部类: public class TestButtonActivity extends Activity { Button btn1, btn2; Toast tst; @Override protected void onCreate(Bundl

javaservlet处理四种常用api请求get,put,post,delete

一般在网站搭建中servlet只需处理post,get请求便足已.本篇注重使用javaweb编写restful风格api,在servlet中对四种常用请求进行处理. 在api中对于一个请求要做的通常是提取header参数,提取携带数据(post,put).在这里已post为例来说明用法. 下面是发起的post请求,用python(很简单)写的,该请求既有自定义header参数Bearer,也有携带数据bodys里的内容 import json import requests taken='YWM

浏览器怎么设置代理IP?四种浏览器设置代理IP的方法

有时候上网需要更换IP,但是本地IP不需要更换,只是通过更换浏览器IP的方式来隐藏自己的IP,突破网络的限制,那么浏览器怎么设置代理IP?不同的浏览器设置代理IP的方法略有不同,下面黑洞代理小编整理了四种浏览器设置代理IP的方法.1.IE浏览器IE浏览器无疑是最常用的浏览器,而像360浏览器.搜狗浏览器.QQ浏览器这样的浏览器都是基于IE内核开发的浏览器,设置代理IP的方法也大同小异.IE浏览器上选择"工具--Internet选项--连接--局域网设置".在"局域网设置&qu

Android onClick 按钮单击事件 四种常用写法

这里使用四种onClick单击事件,来实现电话拔号器的DEMO. XML文件 <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="phone" android:ems="10" android:id="@+id/et_number" android:l

四种常用的post数据提交方式

application/x-www-form-urlencoded 这是默认的post传输方式,用url转码的方法,让数据以key1=val1&key2=val2的方式传输.此方式的数据形式与get方式一样. multipart/form-data 这个也是常见的方式,最常用于传输图片和其他文件.下面是一段数据事例: POST http://www.example.com HTTP/1.1 Content-Type:multipart/form-data; boundary=----WebKit

米洛个人修炼术:情绪的四种常用处理方式,其实都是有问题的

今天周末,公司的几个同事约过来加班,但是从一上班就没有状态,总觉得事情多,心里好烦. 怎么破? 按照通用的说法,我们需要避开情绪,先去工作把事情做了,因为无论如何,工作不能耽误啊. 这么一说感觉确实是对的. 其实,这是错误的. 情绪会以隐藏的方式进入到工作中,进入到你的产品中.尤其是程序员朋友或是管理者,本来就要处理一大堆的问题,很容易烦躁.如果不先把情绪处理掉,就更容易在工作中发脾气,从而影响工作的质量. 那么,我们先来看看,通用的情绪处理方式是什么.往往有几大建议: 1.自己劝自己看开一些.

Android电话拨号器(uri格式)与四种设置点击事件的方法

EditText et = (EditText) findViewById(R.id.callnum); String phone = et.getText().toString().trim(); //创建意图 Intent intent = new Intent(); intent.setAction(intent.ACTION_CALL); intent.setData(Uri.parse("tel:"+phone)); startActivity(intent); 第一种:th