这篇文章紧接着上面两篇文章(Android事件机制和Android View的工作原理来说明自定义View的一些注意事项)下面通过两个例子来说明自定义view的过程。
- CircleView 绘制一个圆形的View
- ScrollView绘制一个可以滑动的列表
CircleView
1.Circle可以做到的事情:支持padding属性,支持wrap_content布局模式,在指定的地点绘制一个圆,支持app:color属性,用来指定view的背景颜色,效果图
2.预备知识点
- Circle继承自View
- 为了支持wrap_content需要自己重写onMeasure方法,因为,wrap_content模式下,要测量的时候,父元素传进来的是AT_MOST模式和parentSize,如果不处理就和match_parent模式一样,所以需要设置一个默认的大小,当为wrap_content的时候设置为这个大小
- onMeasure的重写方法,需要分解出下面两个值,根据值来确定measure的大小,测量完成之后要用setMeasureDimension方法来设置值的大小
MeasureSpec.getMode
MeasureSpec.getSize
- padding属性是和具体的子元素的布局有关,因此在View基类里面没有实现,为了支持这个属性需要自己设置大小(在绘制的时候设置大小)
- 为了支持自定义属性,需要接收AttributeSet来获得这些属性值,也要声明自己的命名前缀
3.代码实现
3.1CircleView的主文件
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.view.View;/** * Created by kisstheraik on 16/8/8. * Description 一个圆形的view支持大小变化和颜色变化 */public class CircleView extends View { private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); private int color=Color.RED; public CircleView(Context context){ super(context); } public CircleView(Context context,AttributeSet attrs){ this(context,attrs,0); } public CircleView(Context context,AttributeSet attributeSet,int defStyle){ super(context,attributeSet,defStyle); TypedArray list=context.obtainStyledAttributes(attributeSet,R.styleable.CircleView); color=list.getColor(R.styleable.CircleView_color,Color.RED);//解析出自定义的color属性//这里需要回收资源 list.recycle(); } @Override protected void onDraw(Canvas canvas){ //处理padding int paddingLeft=getPaddingLeft(); int paddingRight=getPaddingRight(); int paddingTop=getPaddingTop(); int paddingBottom=getPaddingBottom(); int width=getWidth()-paddingLeft-paddingRight; int height=getHeight()-paddingBottom-paddingTop; int radius=Math.min(width, height)/2; paint.setColor(color); //画出圆 canvas.drawCircle(width / 2 + paddingLeft, height / 2 + paddingTop, radius, paint); } //为了支持wrap_content模式,重写measure方法 @Override protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); int wmode=MeasureSpec.getMode(widthMeasureSpec); int hmode=MeasureSpec.getSize(heightMeasureSpec); int wsize=MeasureSpec.getMode(widthMeasureSpec); int hsize=MeasureSpec.getSize(heightMeasureSpec);//根据不同的模式设置不同的值 if(wmode==MeasureSpec.AT_MOST&&hmode==MeasureSpec.AT_MOST){ setMeasuredDimension(200,200); }else if(wmode==MeasureSpec.AT_MOST){ setMeasuredDimension(200,hsize); }else if(hmode==MeasureSpec.AT_MOST){ setMeasuredDimension(wsize,200); } } public void setColor(int color){ this.color=color;//可以在代码里面修改view的颜色 invalidate(); }}
3.2xml布局文件
ScrollView
1.scrollView可以做到的事情,scrollview可以里面添加子元素,每个子元素会横向填充整个屏幕,然后能够实现子元素之间水平滑动,子元素内部也可以滑动,比如子元素是一个listview,这样可以实现一个自定义的ViewPager。效果图
2.预备知识点
- 什么是滑动冲突和怎么解决,滑动冲突就是在有多个元素的时候,事件不能正确的传递到要处理事件的view,导致不能正常滑动,比如一个可以横向滑动的view的里面有可以纵向滑动的list,这时候滑动就不知道该滑动哪一个等等。滑动冲突的解决方式主要是自己定制TouchEvent的分发机制,可以在父元素拦截事件确定分发的逻辑,也可以在子元素干扰父元素的事件分发来达到相同的效果,ScrollView采用的策略是在父元素里面写分发的逻辑
- 利用scroller进行滑动,滑动的逻辑实际上构成了一个可以退出的循环 onDraw->computeScroll->invalidate->onDraw 然后每次在computeScroll里面计算需要滑动到的位置,进行滑动,scroller里面就保存了整个滑动序列,这个序列会随时间变化,具体来说就是记录了滑动是否完成,和下一次需要滑到的地方。
- 事件分发的处理,这里需要从功能角度确定事件分发,具体的逻辑会在代码里面说明
- 布局的处理,onLayout里面进行子元素的布局,根据measure的尺寸进行子元素的布局
3.代码
3.1主代码
import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewGroup;import android.widget.Scroller;/** * Created by kisstheraik on 16/8/8. * Description 可以左右滑动的view */public class ScrollView extends ViewGroup { private Scroller scroller; private VelocityTracker velocityTracker;//用来测量速度 private int lastX=0; private int lastY=0; private int childIndex=0; private int childWidth=0; private int childSize=0; public ScrollView(Context context){ super(context); init(); } public ScrollView(Context context,AttributeSet attributeSet){ super(context,attributeSet); init(); } public ScrollView(Context context,AttributeSet attributeSet,int def){ super(context,attributeSet,def); init(); } public void init(){ if(scroller==null) { scroller = new Scroller(getContext()); velocityTracker=VelocityTracker.obtain(); } } //是否拦截某个事件,这个元素是父元素,使用外部拦截的方式来解决滑动冲突 @Override public boolean onInterceptTouchEvent(MotionEvent ev){ boolean inter=false; int x=(int)ev.getX(); int y=(int)ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: inter=false; //当还在滑动的时候就拦截按下的事件,然后退出滑动 if(!scroller.isFinished()){ scroller.abortAnimation(); inter=true; } break; case MotionEvent.ACTION_MOVE: int delX=x-lastX; int delY=y-lastY; if(Math.abs(delX)>Math.abs(delY)){//当x方向的滑动距离更大,说明还要继续滑动 inter=true; }else inter=false; break; case MotionEvent.ACTION_UP://不拦截这个事件,但是子元素处理不了的时候会调用父元素的onTouchEvent方法 inter=false; break; default: inter=false; break; } lastY=y; lastX=x; return inter; } //处理接收到的事件 @Override public boolean onTouchEvent(MotionEvent ev){ velocityTracker.addMovement(ev); int x=(int)ev.getX(); int y=(int)ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: if(!scroller.isFinished()){ scroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: int dex=x-lastX; scrollBy(-dex,0);//这里是跟着手指在移动,因此需要瞬移 break; case MotionEvent.ACTION_UP: //需要在停下来的时候滑动到最近的child的边界上 int sx=getScrollX(); velocityTracker.computeCurrentVelocity(1000); float xv=velocityTracker.getXVelocity(); if(Math.abs(xv)>=50){//速度达到一定的值就跳到下一个元素 childIndex=xv>0?childIndex-1:childIndex+1; }else { childIndex=(sx+childWidth/2)/childWidth;//否则看看哪一个元素占用的比例多 } childIndex=Math.max(0,Math.min(childIndex,childSize-1)); int dx=childIndex*childWidth-sx; smoothscroolBy(dx,0);//然后滑动到那里 velocityTracker.clear(); break; default:break; } lastX=x; lastY=y; return true; } private void smoothscroolBy(int dx,int dy){ scroller.startScroll(getScrollX(),0,dx,0,500); invalidate(); } @Override public void computeScroll(){ if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); postInvalidate(); } } //布局已经完成 @Override protected void onLayout(boolean changed,int l,int t,int r,int b){ final int count=getChildCount(); int childLeft=0; childSize=count; childWidth=getChildAt(0).getWidth(); for(int i=0;i
3.2使用代码
布局:
代码:
scrollView=(ScrollView)findViewById(R.id.myscroll); LayoutInflater layoutInflater=getLayoutInflater(); WindowManager wm = (WindowManager) getApplication() .getSystemService(Context.WINDOW_SERVICE); for (int i = 0; i < 3; i++) { ViewGroup layout = (ViewGroup) layoutInflater.inflate( R.layout.scroll_view_content, scrollView, false); layout.getLayoutParams().width = wm.getDefaultDisplay().getWidth(); TextView textView = (TextView) layout.findViewById(R.id.title); textView.setText("page " + (i + 1)); createList(layout); scrollView.addView(layout); } private void createList(ViewGroup layout) { ListView listView = (ListView) layout.findViewById(R.id.list); ArrayListdatas = new ArrayList (); for (int i = 0; i < 50; i++) { datas.add("name " + i); } ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1,datas); listView.setAdapter(adapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "click item", Toast.LENGTH_SHORT).show(); } }); }
子元素的布局: