Android自定义View示例
一、继承View复写onDraw方法
新建Paint对象用于绘制自定义图像
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
复写onDraw方法(注意手动实现padding属性,部分代码)
protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int paddingLeft = getPaddingLeft(); //使padding属性生效 //在计算宽高时,考虑padding int width = getWidth()-paddingLeft-paddingRight; //绘制自定义图形 canvas.drawCircle(paddingLeft+width/2,+paddingTop+height/2,radius,mPaint); }
复写onMeasure方法,以实现wrap_content
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //获得Spec模式 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); //获得spec宽 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ //复写实现wrap_content,赋予默认值 setMeasuredDimension(200,200); } //多情况判断 }
以上已粗略完成一个简单的自定义View,为了使用更为方便,为自定义View添加自定义属性
1,在values目录下新建attrs.xml文件,用于定义自定义属性
<?xml version="1.0" encoding="utf-8" ?> <resources> <declare-styleable name="CircleView" > <attr name="circle_color" format="color" /> </declare-styleable> </resources>
2,在自定义View的构造方法中,加载自定义属性(部分代码)
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView); //获得自定义属性集合,解析属性设置默认值,最后实现资源 mColor = a.getColor(R.styleable.CircleView_circle_color,Color.GREEN);
//获得并使用资源,并设置默认值 a.recycle(); //实现资源 }
注:1、为了自定义属性保证生效,在两参数构造方法中调用三参数构造方法。
2、在布局使用自定义属性时,应使用新的命名空间。
二、继承ViewGroup派生特殊的Layout
用于实现自定义的布局,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。在此编写一父布局左右滑动,子元素上下滑动的自定义布局。以下分段分析代码。
1、复写onInteceptTouchEvent方法,事件分发方法,用于解决滑动冲突的问题。
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; //获得滑动坐标 int x = (int)ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; //优化滑动体验 if (!mScroller.isFinished()) { //此处为滑动结束后,若滑动动画未结束下一次事件仍由父容 //器拦截,但实际效果不佳,快速切换时误操作频繁 //取消拦截可减少,但会有快速切换仍会有些许迟滞感 mScroller.abortAnimation(); //intercepted = true; } break; } case MotionEvent.ACTION_MOVE: { //记录滑动距离 int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; //水平滑动距离大于垂直时,认为是水平滑动事件,父容器拦截 if (Math.abs(deltaX) > Math.abs(deltaY)*2) { intercepted = true; } else intercepted = false; break; } case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } //记录坐标 mLastX = x; mLastY = y; mLastYIntercept = y; mLastXIntercept = x; return intercepted; }
2、复写onTouchEvent方法,负责处理父容器的点击事件,在移动(ACTION_MOVE)时,通过scrollBy方法动态移动布局。在操作结束时(ACTION_UP),判断当前的位置已确定是否移动画面,以避免子元素滑动至一半的情形。
smoothScroll方法为自定义弹性滑动方法,用Scroller实现。
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(); //设置速度事件间隔 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; } //最终值大于0小于子项总数 mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1)); //计算自动滑动距离,缓慢滑动 int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx,0); mVelocityTracker.clear(); break; } default: break; } mLastX = x; mLastY = y; return true; }
3、复写onMeasure方法,实现父布局以及子元素的measure过程,主要逻辑为判断SpecMode类型,分情况计算父布局的宽,高。子元素通过measureChildren方法完成measure过程。
此处有待改进的地方:没有考虑padding以及margin属性的作用,而无子元素时也不应将高宽直接赋值为0,应进一步判断进行赋值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = 0; int measureHeight = 0; final int childCount = getChildCount(); //执行子元素的Measure方法 measureChildren(widthMeasureSpec,heightMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); //无子项,高宽为0 if (childCount == 0){ setMeasuredDimension(0,0); }//以下多次判断,由父布局的SpecMode,计算得父布局的高宽并设置 else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ final View childView = getChildAt(0); measureWidth = childView.getWidth()*childCount; measureHeight = childView.getHeight(); setMeasuredDimension(measureWidth,measureHeight); }else if (heightSpecMode == MeasureSpec.AT_MOST){ final View childView = getChildAt(0); measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpecSize,measureHeight); }else if (widthSpecMode == MeasureSpec.AT_MOST){ final View childView = getChildAt(0); measureWidth = childView.getWidth()*childCount; setMeasuredDimension(measureWidth,heightSpecSize); } }
4、复写onLayout方法,用于完成父布局的layout过程,即是遍历所有子元素,完成子元素的layout过程。
protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < mChildrenSize ; i++){ final View childView = getChildAt(i); //判断是否可见 if (childView.getVisibility() != View.GONE){ //执行子元素Layout final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight()); childLeft += childWidth; } } }
以上基本完成了一个简单的自定义View,还有一些细节问题,如Scroller、VelocityTracker相关方法的使用等。
总结一下,实现继承Viewgroup的自定义layout,只需分别手动实现Measure、Layout、onTouchEvent方法,完成父容器以及子元素的构建过程即可。为了解决滑动冲突的问题,也只需复写onInterceptTouchEvent方法,实现自定义的事件分发逻辑。
其中,Measure过程调用measureChildren方法完成子元素的测量过程,父布局则根据SpecMode具体计算宽高。
Layout过程遍历子元素,调用子元素的layout方法即可。
onTouchEvent方法,则是书写移动以及操作结束时的View动态变化的过程。
                    
                
                
            
        
浙公网安备 33010602011771号