自定义控件(面试热门问题之一)

一、为什么要自定义控件

1.特定的显示风格 :比如app需要一些特殊的效果,是原生组件所没有的。
2.处理特有的用户交互:一些特殊的与用户交互的方式。比如说,原本TextView不能滑动里面的文字,我们需要它的文字可以被滑动。
3.优化我们的布局:我们可以通过各种嵌套来实现我们的布局,但是绘制和测量会比较慢,可以通过自定义控件,通过某种方式来提高我们的效率(指的是绘制、测量等)。
4.封装等…… :一些用的次数比较多的空间组合,比如顶部标题栏等,可以自定义一个控件封装它。

二、如何自定义控件

1.自定义属性的声明与获取
2.测量onMeasure
3.布局onLayout(ViewGroup)
4.绘制onDraw
5.onTouchEvent
6.onInterceptTouchEvent(ViewGroup)

1.自定义属性声明与获取

1)分析需要的自定义属性
2)在res/values/attrs.xml定义声明
3)在Layout xml文件中进行使用
4)在View的构造方法中进行获取
xml文件中:

<?xml version="1.0" encoding="utf-8"?>
<resource>
    <attr name="icon" format="reference"></attr>
    <attr name="color" format="color"></attr>
    <attr name="text" format="string"></attr>
    <attr name="text_size" format="dimension"></attr>
    <declare-styleable name="ChangeColorIconWithText">
        <attr name="icon"></attr>
        <attr name="color"></attr>
        <attr name="text"></attr>
        <attr name="text_size"></attr>
    </declare-styleable>
</resource>

java代码中:

TypeArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeColorIconWithText);
int n = a.getIndexCount();
for(int i = 0; i < n; i++){
    int attr = a.getIndex(i);
    switch(attr){
        case R.styleable.ChangeColorIconWithText_icon:
            BitmapDrawable drawable = (BitmapDrawable)a.getDrawable(attr);
            mIconBitmap = drawable.getBitmap();
            break;
        case R.styleable.ChangeColorIconWithText_color:
            mColor = a.getColor(attr, 0xFF45C01A);
            break;
        case R.styleable.ChangeColorIconWithText_text:
            mText = a.getString(attr);
            break;
        case R.styleable.ChangeColorIconWithText_text_size:
            mTextSize = (int) a.getDimension(attr, TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics()));
            break;
    }
}
a.recycle();
2.测量onMeasure

1) EXACTLY, AT_MOST, UNSPECIFIED
2) MeasureSpec
3) SetMeasuredDimension
4) requestLayout()

view需要测量自身到底多大、什么样子。
测量由两部分组成,一个是测量的模式MODE和测量的具体值。
EXACTLY 设置了一个明确的值,例如100dp,match_parent等。
AT_MOST 最多不超过一个值,例如 wrap_content,宽度高度由自身决定,并且最大不能超过父控件。
UNSPECIFIED 大小没有限制,例如在srcollview、 listview等中的子控件高度是不可能限制。

MeasuerSpec是一个辅助类,测量的值一般会封装为MeasureSpec,下面是示例代码:

private int measureHeight(int heightMeasureSpec){
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);  
    int size = MeasureSpec.getSize(heightMeasureSpec);  
    //如果模式为EXACTLY,直接使用size
    if(mode == MeasureSpec.EXACTLY){
        result = size;
    }else{
        //计算自身需要的高度
        result = getNeedHeight() + getPaddingTop() + getPaddingBottom();
        //如果模式为AT_MOST,需要与size取大的值
        if(mode == MeasureSpec.AT_MOST){
            result = Math.min(result, size);
        }
    }
    return result;
}

测量后将result通过setMeasuredDimension方法设置。
requestLayout方法的执行会重新测量自身大小。但不会重新绘制。

3.布局onLayout(ViewGroup)

1)决定子View的位置
2)尽可能将onMeasure中一些操作移动到此方法中。因为onMeasure要被调用好多次,而onLayout触发一次
3)requestLayout() 执行的时候,进行onLayout方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
    final int childCount = getChildCount();
    for(int i = 0; i < childCount; i++){
        final View child = getChildAt(i);
        if(child.getVisiblity() == GONE){
            continue;
        }

        left = caculateChildLeft();//计算childView layout的左上角x坐标
        top = caculateChildTop();//计算childView layout的左上角y坐标

        child.layout(left, top, left + cWidth, top + cWidth);
    }
}
4.绘制onDraw

1)绘制内容区域
2)invalidate(), postInvalidate();
3)Canvas.drawXXX
4)translate、rotate、scale、skew
5)save()、restore()

绘制内容的显示,通过调用Canvas相关API。其间可以巧妙的利用translate、rotate,之后别忘了调用save()、restore()对canvas进行保存,对于这两个方法对理解呢,可以去看moble_xie大神博客(:P)
http://blog.csdn.net/lovexieyuan520/article/details/50683249

@Override
protected synchronized void onDraw(Canvas canvas){
    //使用Canvas相关API绘制 anything you want
}

重新绘制的两个方法, 主线程中invalidate(),子线程中postInvalidate()。

5.onTouchEvent

1)ACTION_DOWN、ACTION_MOVE、ACTION_UP
2)ACTION_POINTER_DOWN、ACTION_POINTER_UP
3)parent.requestDisallowInterceptTouchEvent(true); 告诉父控件不要拦截touch事件,而由子控件处理事件。
4)VelocityTracker :hudashi大神博客链接, http://blog.csdn.net/hudashi/article/details/7352157

@Override
public boolean onTouchEvent(MotionEvent ev){
    initVelocityTrackerIfNotExists();
    mVelocityTracker.addMovement(ev);

    final int action = ev.getAction();
    switch(action){
        case MotionEvent.ACTION_DOWN:
            //进行一些初始化赋值等的操作
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            //如果需要进行速度判断
            int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
            //释放各种资源、重置变量
            break;
        case MotionEvent.ACTION_CANCEL:
            //释放各种资源、重置变量
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            //如果支持多指触控,在此设置activePointer
            final int index = ev.getActionIndex();
            mLastMotionY = (int) ev.getY(index);
            mActivePointerId = ev.getPointerId(index);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            //如果支持多指触控,且抬起的是activitePointer,则重置选择一个手指为活跃的手指
            if(pointerId == mActiviPointerId){
                final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                mLastMotionY = (int) ev.getY(newPointerIndex);
                mActivePointerId = ev.getPointerId(newPointerIndex);
                if(mVelocityTracker != null){
                    mVeloacityTracker.clear();
                }
            }
            break;
    }
    return true;
}
6.onInterceptTouchEvent(ViewGroup)

1)ACTION_DOWN、ACTION_MOVE、ACTION_UP
2)ACTIOON_POINTER_DOWN、ACTION_POINTER_UP
3)决定是否拦截该手势

@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
    int action = ev.getAction();

    swicth(action & MotionEvent.ACTION_MASK){
        case MotionEvent.ACTION_MOVE:
            final int activePointerId = mActivePointerId;
            if(activePointerId == INVALID_POINTER){
                break;
            }
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            final int y = (int) ev.getY(pointerIndex);
            final iny yDiff = Math.abs(y - mLastMotionY);
            if(yDiff > mTouchSlap){
                mIsBeingDragged = true;
                mLastMotionY = y;
            }
            break;
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_CANCEL:
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return mIsBeingDragged;
}

这个方法返回true,代表viewgroup拦截了事件,不交给子控件处理。

三、其它
1.onSaveInstanceState、onRestoreInstanceState
这两个方法是Activity也拥有的,当acitivity容易被销毁时第一个方法会被调用,可以进行一些临时数据的保存,例如以下的几种情况:

1)当用户按下Home键的时候
2)长按Home键,选择运行其它程序时
3)按下电源键,关闭屏幕显示时
4)从A activity 启动新的activity时
5)屏幕方向切换时
6)…

而第二个方法,onRestoreInstanceState,则是在执行第一个方法onSaveInstanceState后,确实被系统销毁了,并且再回到原来的activity时,会执行的方法,可以将之前保存的数据通过参数获取出来。
view的两个方法,是在activity执行这两个方法时执行的,也是用于存储临时数据,比如view的一些状态等。

@Override
protected Parcelable onSaveInstanceState(){
    Bundle bundle = new Bundle();
    //将要保存等数据存在bundle中
    //...
    return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state){
    if(state instanceof Bundle){
        Bundle bundle = (Bundle) state;
        //获取并使用Bundle对象里面的数据
        //...
        super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATUS));
        return;
    }
    super.onRestoreInstanceState(state);
}

2.ViewConfiguration(mTouchSlop等)
3.ScaleGestrueDetector
4.ViewDragHelper 链接到鸿洋大神博客 http://blog.csdn.net/lmj623565791/article/details/46858663
5.以及更多…

/………..后记: 初步了解就到这里(代码源自幕客网视频),文中代码全手打,如果直接粘贴可能有错误的地方 : P ………………/

猜你喜欢

转载自blog.csdn.net/qq_23057645/article/details/65936471
今日推荐