赞
踩
这篇文章分两方面描述Android中的自定义view,分别是自定义view和自定义viewGroup,它们又可以分为继承view(或viewgroup)和继承已有控件。
关于自定义view,涉及到很多图形方面的知识,其中用的最多的是paint和canvas。其api较多,这里不写出,请去官网查询:graphic
实现过程也很简单,onDraw方法中在图片上绘制一个梯形角标一个,在角标上绘制文字,就OK了。代码
public class MyimageView extends ImageView { public MyimageView(Context context) { super(context); } public MyimageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //设置画笔 Paint mpanit=new Paint(); mpanit.setColor(Color.YELLOW); mpanit.setStyle(Paint.Style.FILL);//绘制类型为填充 //找出宽高中最小边. int minEdge=getWidth()>getHeight()?getHeight():getWidth(); Path mpath=new Path(); //view绘制的时候坐标(0,0)在左上方 // 以下几步画一个梯形 mpath.moveTo(0,minEdge/4);//移动路径起点 mpath.lineTo(0,minEdge/2);//画线 mpath.lineTo(minEdge/2,0); mpath.lineTo(minEdge/4,0); mpath.close();//把开口的路径关闭。终点与原点相连。 //绘制这个梯形 canvas.drawPath(mpath,mpanit); Paint textpanit=new Paint(); textpanit.setColor(Color.RED); textpanit.setTextSize(30); canvas.rotate(-45,getWidth()/2,getHeight()/2);//画布以图片中心逆时针旋转45度 canvas.drawText("图片说明",getWidth()/4+getHeight()/11,getHeight()/11,textpanit); } }
要注意view绘制的原点在左上角。使用该自定义view。
<com.example.mycustomview.MyimageView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center"
android:layout_marginTop="50dp"
android:src="@drawable/show"
android:background="#13ea41"/>
public class MyView extends View{ public MyView(Context context) { super(context); } public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint1=new Paint(); paint1.setStyle(Paint.Style.FILL); paint1.setColor(Color.RED); Paint paint2=new Paint(); paint2.setStyle(Paint.Style.FILL); paint2.setColor(Color.YELLOW); Paint paint3=new Paint(); paint3.setStyle(Paint.Style.FILL); paint3.setColor(Color.BLUE); float radius=Math.min(getWidth(),getHeight())/2; RectF rectF=new RectF(0,0,radius*2,radius*2); //drawArc方法第一个参数(矩形)限定画圆的范围,第二个是起始角度,第三个是绘制角度 //第四个参数发false画圆弧,true画扇形,第五个参数是画笔。 canvas.drawArc(rectF,0,120,true,paint1); canvas.drawArc(rectF,120,120,true,paint2); canvas.drawArc(rectF,240,120,true,paint3); startAnimator(); } //view伸缩动画 private void startAnimator(){ //设置缩放原点 this.setPivotX(this.getWidth()/2); this.setPivotY(this.getHeight()/2); //组合动画缩放 AnimatorSet animatorSet = new AnimatorSet(); ObjectAnimator scaleX=ObjectAnimator.ofFloat(this,"scaleX",1f,1.6f,1f); ObjectAnimator scaleY=ObjectAnimator.ofFloat(this,"scaleY",1f,1.6f,1f); //scaleX.setRepeatMode(ObjectAnimator.RESTART); //scaleY.setRepeatMode(ObjectAnimator.RESTART); scaleX.setRepeatCount(ObjectAnimator.INFINITE);//无限循环播放 scaleY.setRepeatCount(ObjectAnimator.INFINITE); scaleX.setDuration(800); scaleY.setDuration(800); animatorSet.play(scaleX).with(scaleY); animatorSet.start(); } }
当然这里还有一种动画的实现方式那就是多次ondraw绘制,比如我想做一个像扇形打开的那种扇形绘制动画,就分10次绘制,每次多绘制扇形的1/10。下面是效果图(不会截gif图,所以截了一张过程图)和代码
int times=0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint1=new Paint(); paint1.setStyle(Paint.Style.FILL); paint1.setColor(Color.RED); Paint paint2=new Paint(); paint2.setStyle(Paint.Style.FILL); paint2.setColor(Color.YELLOW); Paint paint3=new Paint(); paint3.setStyle(Paint.Style.FILL); paint3.setColor(Color.BLUE); float radius=Math.min(getWidth(),getHeight())/2; RectF rectF=new RectF(0,0,radius*2,radius*2); //drawArc方法第一个参数(矩形)限定画圆的范围,第二个是起始角度,第三个是绘制角度 //第四个参数发false画圆弧,true画扇形,第五个参数是画笔。 canvas.drawArc(rectF,0,120*times/10,true,paint1); canvas.drawArc(rectF,120,120*times/10,true,paint2); canvas.drawArc(rectF,240,120*times/10,true,paint3); times++; if(times<=10){ postDelayed(new Runnable() { @Override public void run() { invalidate();//通知onDraw方法再次绘制 } },300); } }
用这个view如下。
<com.example.mycustomview.MyView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center"
android:layout_marginTop="50dp"/>
这里发现一个问题。如果这个view的长宽设置成wrap_content会怎样呢。试了一下,效果跟match_parent一样,因为自适应没有给他尺寸,所以只能是根据父布局来了。那么如何设置自己想要的的自适应尺寸呢。这里就要重写另一个在自定义view中比较重要的方法onMeasure了。如下
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //获取宽高的测量模式(如wrap_content为一种模式) int widthMode=MeasureSpec.getMode(widthMeasureSpec); int heightMode=MeasureSpec.getMode(heightMeasureSpec); //获取宽高的大小 int widthSize=MeasureSpec.getSize(widthMeasureSpec); int heightSize=MeasureSpec.getSize(heightMeasureSpec); //设置自己用于wrap_conten显示的宽高 int mwidth=500; int mheight=500; if(widthMode==MeasureSpec.EXACTLY){ Log.e("Myview","exactly"); } if(widthMode==MeasureSpec.UNSPECIFIED){ Log.e("Myview","unspecified"); } /* 这里的测量模式有三种。 AT_MOST对应与wrap_content EXACTLY对应与match_parent与自己设置尺寸 UNSPECIFIED未指定尺寸,不常见,在scroview里面控件尺寸受内容影响而不受设定尺寸影响 */ if(widthMode==MeasureSpec.AT_MOST){ widthSize=mwidth; } if(heightMode==MeasureSpec.AT_MOST){ heightSize=mheight; } setMeasuredDimension(widthSize,heightSize); }
其实上面的view还有一个问题就是padding不生效,因为上述例子在onDraw并没有对padding进行处理,所以肯定不会生效了。要想让padding生效,在onDraw方法中可以用getPaddingLeft()获取左边padding(其他边padding以此类推)然后绘制的时候考虑padding就OK了。
最后说一下自定义属性,就比如用某些别人的控件框架的时候,xml里面有Android:开头的属性(系统自带),app:开头的属性(自定义)。这里说自定义属性该如何去做。首先在values文件夹下创建一个xml文件用于存放自定义属性。
attrs_MycustomView.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--设置一个集合用于放myview的自定义属性,等会根据name解析属性-->
<declare-styleable name="MyView">
<!--format表示你自定义属性的格式这里是color(还有很多其他格式如boolean,enum等)。-->
<!--format的值关乎你xml文件中用它的时候用什么类型来赋值。-->
<attr name="text_color" format="color"/>
</declare-styleable>
</resources>
然后在自定义的构造函数中读取它。需要的时候用就行了。
int mColor;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//读取刚刚定义的myview属性集合
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyView);
//读取属性值。一个参数为资源id(注意格式为:集合名_属性名)。第二个参数为默认属性值.
mColor=typedArray.getColor(R.styleable.MyView_text_color,Color.BLACK);
typedArray.recycle();//释放资源
}
我的例子是在之前的view的基础上加上文字。然后文字的颜色由自定义属性确定。在之前的onDraw方法最后追加如下。
Paint paint=new Paint();
paint.setColor(mColor);
paint.setTextSize(60);
canvas.drawText("添加文字",30,getHeight()-30,paint);
xml文件用的时候。
<com.example.mycustomview.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginTop="50dp"
app:text_color="#20d217"/>
viewgroup是用来规范其中的子view的,那么自定义viewgroup就是用自己的规则去规范它的子view。
自定义viewgroup需要继承Viewgroup。然后重写两个方法
onMeasure:与自定义view一样完成测量工作,不同的是除了自己的测量还有它的子view的测量。所以必须在onmeasure中调用measureChildren方法设置子view的测量模式或者调用measureChild对单个子view设置测量模式。
onLayout:完成子view的位置确定。关键方法就是子view的layout方法确定位置。
这里给出一个我用自定义viewgroup的方式实现一个仿QQ侧滑(网上一般采用ScrollView)。侧滑效果如下
再给出我的代码及部分注释
public class MyViewGroup extends ViewGroup{ //屏幕宽高 private int screenWidth; private int screenHeight; //滑动起始位置 int startX=0; public MyViewGroup(Context context) { super(context); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); //获取屏幕宽高 DisplayMetrics displayMetrics=new DisplayMetrics(); WindowManager manager= (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); manager.getDefaultDisplay().getMetrics(displayMetrics); screenWidth=displayMetrics.widthPixels; screenHeight=displayMetrics.heightPixels; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //让子控件自己测量自己 measureChildren(widthMeasureSpec,heightMeasureSpec); //固定布局宽高为屏幕宽高 setMeasuredDimension(screenWidth,screenHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if(getChildCount()<2) return; //只考虑两个布局 //侧滑界面左边在屏幕外1/3,用于滑动 View child1=getChildAt(0); child1.layout(-child1.getMeasuredWidth()/3,0,child1.getMeasuredWidth()*2/3,child1.getMeasuredHeight()); View child2=getChildAt(1); child2.layout(0,0,child2.getMeasuredWidth(),child2.getMeasuredHeight()); } @Override public boolean onTouchEvent(MotionEvent event) { View child1=getChildAt(0); View child2=getChildAt(1); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: startX=(int)event.getX(); break; case MotionEvent.ACTION_MOVE: int x=(int)event.getX(); int distance=x-startX; startX=x; int lastX=child2.getLeft()+distance;//主界面滑动位置确定 int lastX2=child1.getLeft()+distance/3;//侧滑界面滑动位置确定 lastX2=distance>0?lastX2+1:lastX2-1;//距离多个1,为了实现无缝 //确保位置合法 if(lastX<0) lastX=0; if(lastX>child1.getMeasuredWidth()) lastX=child1.getMeasuredWidth(); if(lastX2<-child1.getMeasuredWidth()/3) lastX2=-child1.getMeasuredWidth()/3; if(lastX2>0) lastX2=0; //改变位置并重绘 child1.layout(lastX2,0,lastX2+child1.getMeasuredWidth(),child1.getMeasuredHeight()); child2.layout(lastX,0,lastX+child2.getMeasuredWidth(),child2.getMeasuredHeight()); invalidate(); break; case MotionEvent.ACTION_UP: break; } return true; } }
调用的时候传人两个布局就行。如下
<com.example.mycustomview.MyViewGroup android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="300dp" android:layout_height="match_parent" android:background="#2dbf30"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="这是侧滑界面"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ed493d"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="这是主界面"/> </LinearLayout> </com.example.mycustomview.MyViewGroup>
这里我实现的这个侧滑跟QQ的比还是有不少缺陷,因为这个实例主要目的是自定义viewgroup的练手,弄清楚自定义viewgroup就行了,没有去把它弄的很完美。这里指出两个最大的缺陷
1.没有弹性和加速度效果。如果想实现这个功能可以借用这两个类VelocityTracker(加速度相关),Scroller(滑动相关)2.
2.没有做margin处理,因为侧滑和主界面都是直接丢LinearLayout也没用到margin所以也没有考虑margin效果。如果考虑margin的话就得修改LayoutParams,具体怎么弄百度上相关文章很多(我很懒!)
3.没有做事件冲突处理。这个东西类似于滑动控件嵌套,如果TextView换成button肯定会出现事件冲突的。解决冲突方法跟一般的情况一样
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。