View工作原理(三)——自定义View

本文介绍了自定义View的概念,强调了为什么需要自定义View以实现特殊功能和效果。详细讨论了自定义View时应注意的问题,如支持wrap_content、padding和margin,避免使用Handler,正确处理线程和动画,以及解决滑动冲突。此外,还概述了自定义View的四种主要类型,并提醒开发者在编写自定义View时要关注MeasureSpec模式和onMeasure、onDraw方法的使用,确保性能和准确性。

自定义View

什么是?

自定义View就是自己设计出来的View,一般继承View或ViewGroup或已有的控件和布局。自己处理View的三个过程:测量、布局、绘制。并处理可能产生的一些问题:如滑动冲突,事件处理等等。需要对View的工作原理、事件分发及滑动冲突处理有一定了解。如果需要很炫的动画,也需要对动画有一定的了解。

为什么要用自定义View?

实现一些系统控件不能实现的特效,功能,而且自定义View十分的灵活,比如你要实现一个选票控件,课程表控件,日期选择控件等等,当然你也可以使用开源框架,但是有些业务逻辑用开源框架并不能对业务逻辑进行很好的控制。

自定义View须知(一些需要注意的问题)
1.让View支持warp_content

直接继承View和ViewGroup的控件,如果不处理wrap_content,会导致可能不会实现预期效果,原因在:View工作原理二说过,wrap_content不处理时为AT_MOST模式,也就是和match_partent的大小一样。

2.支持padding和margin

对于直接继承View的需要处理padding,如果不处理会导致padding失效,但margin有作用,因margin是父容器实现的;对于直接继承ViewGroup的需要处理padding和子元素的margin,不然会导致padding和子元素的margin失效。

3.尽量不要在View中使用Handler

因为View内部本身就提供了post系列的方法,可以完全替代Handler作用,除非你很明确需要用Handler发消息。

4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow

当包含此View的Activity退出或者当前View被remove的时候onDetachedFromWindow方法会被调用,这时就是停止动画和线程的很好的时机;和这个方法对应的是onAttachedWindow,当包括此View的Activity启动时,View的onAttachedToWindow会被调用。当View变得不可见时我们也需要停止线程和动画,防止内存泄漏。

5.View具有滑动嵌套情形时,需要处理好滑动冲突
自定义View的分类
1.继承View重写onDraw方法

需要自己处理wrap_content和padding。

2.继承ViewGroup

需要自己处理ViewGroup的测量和布局过程。

3.继承特定的View(如:TextView、ImageView)

不需要自己支持wrap_content和padding。

4.继承特定的ViewGroup(如:LinearLayout、FrameLayout)

不需要处理ViewGroup的测量和布局这两个过程。
自定义View有四个构造函数,第二个构造函数是我们在xml里写我们的View时会调用的,所以如果后面用到context注意要在这里初始化或者调用三参构造,并在三参构造里初始化。
首先,要先理解一下自定义View的SpecMode有三种模式:
①UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
②EXACTLY: 父容器已经测量出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent具体的数值这两种模式。
③AT_MOST: 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现。它对应于LayoutParams中的warp_content。所以在写自定义View的时候一定要重写onMeasure()方法 ,去适配WRAP_CONTENT的情况,因为WRAP_CONTENT情况为AT_MOST模式,不设置会和MATCH_PARENT的效果一样,也许你在使用的时候对你的界面好像并没有什么影响,单这样复用性很不好,也就是说你下次再要做一个和这个类似的东西你可能还要去适配大小。

还有一点需要注意,不要在onMeasure中去获取宽高,因为可能不准确,最好在onLayout中去获取宽高,onMeasure中的两个参数widthMeasureSpec、heightMeasureSpec我打印出来的值是1073742904,-2147482016,getHeight()打印出来的值为0,点进去一看getHeght{ return mBottom - mTop;},mBottom,mTop的值是在onLayout的时候才去初始化的,所以在onMeasure中难以获取到准确的测量值。我这里继承View的onMeasure走了两遍,子元素的MeasureSpec是由父容器的MeasureSpec和子元素的LayoutParams决定的,两次的getMeasuredHeight()值不一样。
MeasureSpec (32位) = MeasureMode(2位) 模式+ MeasureSize(30位)大小
顶级View(DecorView)的MeasureSpec是由窗口尺寸和其自身的LayoutParams。

然后我们就可重写onDraw方法绘制我们想要的自定义View的样式,视图。如果需要自定义自己的属性可以在三参构造使用如下语法进行自定义自己的属性:

//R.stayleable.名字对应的是res/values/attrs.xml中declare-styleable中对应name
//,里面会有自定义的属性
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.名字);
//这样你就可以去获取你定义在xml里的属性然后进行处理或者不处理
//获取方式如下
//T类型 变量名 = typedArray.getT类型(R.styleable.xml里属性名字);

自定义View根据需求可能要处理一些其他的逻辑,如动画效果需要去处理滑动问题,甚至滑动冲突问题,对View的绘制流程也应该有一些了解。
注意onDraw方法调用会比较频繁一点,所以尽量不要在onDraw方法里创建对象。

上面主要说的是自定义View。自定义ViewGroup要注意在onMeasure和onLayout方法去处理子元素的margin和自身的padding,不然无效。
一个自己做的小Demo源码
自定义饼图

import java.util.ArrayList;
import java.util.List;

  public class FanChart extends View implements View.OnTouchListener{

       private Context mContext;
       private Canvas mCanvas;
       private Paint mPaint;
       private int width;
       private int height;
       private static int ItemCount = 0;
       private final static int MAX_Count = 7;
       private static double currValue = 0;
       private int B_mRadius = 200;
       private int S_mRadius = 120;
       private int cx = 400,cy = 400;
       private int[] colors = new int[]{Color.RED ,Color.BLUE,Color.YELLOW,Color.GREEN,Color.GRAY
               ,Color.CYAN ,Color.BLACK, Color.DKGRAY ,Color.MAGENTA, Color.DKGRAY};
       private static List<FanChartItem> items = new ArrayList<>();
       private GestureDetector gestureDetector;
       private int selectItem_index = -1;
       private int outValue = 20;
       private float selectItem_middleAngle = 0;
       private float scale = 0.8f;
       private float scaleRadius = 0.7f;
       private boolean isClickable = true;
       private float startDegree = 0;
       private float NeedDegree = 0 ;
       private boolean isRotate = false;
       private float currStartDefree = 0;

       public FanChart(Context context) {
           this(context,null);
       }

       public FanChart(Context context, @Nullable AttributeSet attrs) {
           this(context, attrs,0);
       }

       public FanChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
           super(context, attrs, defStyleAttr);
//        TypedArray array = context.getTheme().obtainStyledAttributes(attrs,R.styleable.FanChart
//                ,0,R.styleable.FanChart);
           mContext = context;
           mPaint = new Paint();
           setOnTouchListener(this);
           gestureDetector = new GestureDetector(getContext(),new GestureDetector.SimpleOnGestureListener(){
               @Override
               public boolean onSingleTapConfirmed(MotionEvent e) {
                   float x = e.getX();
                   float y = e.getY();
                   selectItem_index = getClickIndex(x,y);
                   if(selectItem_index <= -1)
                       return true;

                   Toast("当前点击item:"+selectItem_index+"");
                   Log.e("当前item的中间值",selectItem_middleAngle+"");
                   //计算需要旋转的角度
                   NeedDegree= 0;
                   if(selectItem_middleAngle >=0&&selectItem_middleAngle<=90) {
                       NeedDegree = 90 + selectItem_middleAngle;
                   }else{
                       NeedDegree = selectItem_middleAngle -270; //逆时针
                   }
                   Log.e("需要旋转的角度:",NeedDegree+"");
                   //处理点击后旋转动画
                   isRotate = true;
                   ClickRotateAnimation(NeedDegree,0);//t
                   isRotate = false;
                   invalidate();

                   return true;
               }
           });
       }

       //更新value值
       public void upDateCurrValue(){
           currValue = 0;
           for(int i = 0 ; i< items.size(); i++){
               currValue+=items.get(i).value;
           }
       }

       public void upDateWeight(){
           FanChartItem tItem ;
           for(int i = 0 ;i< items.size() ;i++){
               tItem = items.get(i);
               double weight = tItem.value/currValue;
               tItem.setWeight(weight);
           }
       }

       @SuppressLint("DrawAllocation")
       @Override
       protected void onDraw(Canvas canvas) {
           super.onDraw(canvas);
           mCanvas = canvas;
           mPaint.setAntiAlias(true);

           //更新值并重新绘制扇形
           upDateCurrValue();
           upDateWeight();
           drawFan(B_mRadius,false,true);
           drawFan(S_mRadius,true,true);
       }

       @Override
       protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
           super.onLayout(changed, left, top, right, bottom);
       }

       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           super.onMeasure(widthMeasureSpec, heightMeasureSpec);
           width = B_mRadius +50;
           height = B_mRadius +50;
           ViewGroup.LayoutParams lp = getLayoutParams();
           if(lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && lp.width == ViewGroup.LayoutParams.WRAP_CONTENT){
               width = 200;
               height = 200;
               B_mRadius = (int) (width*scale);
               S_mRadius = (int) (B_mRadius*scaleRadius);
           }else if(lp.height == ViewGroup.LayoutParams.WRAP_CONTENT){
               height = 200;
           }else if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT){
               width = 200;
           }
       }

       //添加一个item
       public void addOneOval(FanChartItem item){
           Log.i("添加"," ");
           if(items.size() > MAX_Count){
               Toast("最多只能有七个!");
           }
           items.add(item);
           ItemCount ++;
           invalidate();
       }

       //删除一个item
       public void removeOneOval(String id){
           Log.i("删除"," ");
           if(items.size() == 0){
               Toast("无数据可以移除!");
               return;
           }
           boolean isfind = false;
           for(int i = 0 ; i< items.size() ; i++){
               if(items.get(i).id.equals(id)){
                   ItemCount--;
                   items.remove(i);
                   invalidate();
                   isfind = true;
               }
           }
           if(!isfind){
               Toast("没有找到id为"+id+"的元素!");
           }
       }

       //dp转px
       public int dp2px(int dp){
           float scale = mContext.getResources().getDisplayMetrics().density;
           return (int) (dp*scale+0.5f);
       }

       //画扇形的具体处理
       public void drawFan(int r,boolean sameColor,boolean userCenter)
       {
           startDegree = (startDegree +360)%360;

           float t = startDegree;  //item 0 的开始值
           if(items.size() ==0) //如果没有item就直接退出
               return;
           for(int i = items.size()-1; i>= 0 ; i--)
           {
               if(!sameColor)
               {
                   mPaint.setColor(items.get(i).color);  //用item的颜色画
               }
               else
               {
                   mPaint.setColor(Color.WHITE);  //统一白色画
               }
               mPaint.setStyle(Paint.Style.FILL); // 填充

               if(selectItem_index != i)
               {
                   mCanvas.drawArc(new RectF(cx - r, cy - r, cx + r, cy + r)
                           , t, (float) (items.get(i).weight * 360.0), userCenter, mPaint);
               }
               else
               {   //判断是否为选择的item,是则要画突出
                   mCanvas.drawArc(new RectF(cx - r - outValue, cy -r -outValue , cx +r +outValue , cy +r +outValue)
                           , t+1, (float) (items.get(i).weight * 360.0)-2, userCenter, mPaint);
               }
               t += items.get(i).weight * 360.0 ;
           }
           Log.e("绘画","当前点击item:"+selectItem_index+", start: "+startDegree+",中间值:"+selectItem_middleAngle+", 已旋转:"+NeedDegree);
       }


       @Override
       public boolean onTouch(View v, MotionEvent event) {
           gestureDetector.onTouchEvent(event);

           switch(event.getAction()){
               case MotionEvent.ACTION_DOWN:
                   break;
               case MotionEvent.ACTION_MOVE:
                   break;
               case MotionEvent.ACTION_UP:
                   break;

           }
           return true;
       }

       public void Toast(String s){
           Toast.makeText(mContext ,s,Toast.LENGTH_SHORT).show();
       }

       public int getClickIndex(float x,float y){
           Log.e("x,y",x+","+y);
           double s = Math.sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy));
           if(s >B_mRadius || s < S_mRadius){
               return -2;
           }

           double angle = 0;
           float x1 = Math.abs(x - cx);
           float y1 = Math.abs(y - cy);
           double arctan = Math.atan(y1/x1*1.0);
           if(x1 == 0 && y1 ==0){
               angle = 90;
           }else if(x1 == 0){
               angle = 180;
           }else if(y1 == 0){
               angle = 0;
           }else if(x>cx && y<cy){
               //第一象限
//            Toast("1");
               angle = Math.abs(arctan/Math.PI*180);
           }else if(x<cx && y<cy){
               //第二象限
//            Toast("2");
               angle = 180 - Math.abs(arctan/Math.PI*180) ;
           }else if(x<cx && y>cy){
               //第三象限
//            Toast("3");
               angle = 180 + Math.abs(arctan/Math.PI*180);
           }else{
//            Toast("4");
               angle =360 - Math.abs(arctan/Math.PI*180);
               //第四象限
           }
           Log.e("点击角度",""+angle);

           double t = 360 - startDegree; // 360 - 因为我的判断角度和绘画角度方向计算正好相反故用对顶角
           //分三种情况
           if(t>angle){
               for(int i = items.size()-1 ;i>=0; i--){
                   t-=items.get(i).weight*360;
                   if(t<=angle){
                       selectItem_middleAngle = (float) (t+items.get(i).weight*360.0/2);
                       return i;
                   }
               }
           }else if( t<angle){
               for(int i = 0 ; i< items.size() ; i++){
                   FanChartItem item = items.get(i);
                   t+=item.weight*360.0;
                   if(t>360){
                       t = t %360;
                   }
                   if(t>angle) {
                       selectItem_middleAngle = (float)(t-item.weight*360.0/2);  //获取点击item的中间值
                       return i;
                   }
               }
           }else if(t == angle){
               selectItem_middleAngle = (float) (t + items.get(0).weight*360/2);
               return 0;
           }
           selectItem_middleAngle = (selectItem_middleAngle+360)%360;
           return -1;
       }

       float last = 0;
       float curr = 0;
       float t =0;
       public void ClickRotateAnimation(final float needRotate, final float currRotate){
           float d = 0;
           last = 0;
           curr = 0;
           t = startDegree;
           ValueAnimator animator = ValueAnimator.ofFloat(0,needRotate);
           animator.setInterpolator(new AccelerateDecelerateInterpolator());
           animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener( ) {
               @Override
               public void onAnimationUpdate(ValueAnimator animation) {
                   startDegree = t + animation.getAnimatedFraction()*needRotate;

                   invalidate();
               }
           });
           animator.setDuration(1000);
           animator.start();
       }
   }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值