Android自定义View实战(SlideTab-可滑动的选择器)
2016-08-11 03:50
435 查看
转载请标明出处:
http://blog.csdn.net/xmxkf/article/details/52178553
本文出自:【openXu的博客】
目录:
初步分析重写onDraw绘制
重写onMeasure计算宽高
重写onTouch加入滑动效果
自定义属性
源码下载
这篇博客我们来一发自定义控件的实战,恰好前些天有一个小需求,效果图如下:
根据效果图,我们可以确定,用自定义View完全可以搞定,在自定义控件系列博客第一篇中,我们总结了自定义View的几个步骤:
继承View,覆盖构造方法
自定义属性
重写onMeasure方法测量宽高
重写onDraw方法绘制控件
当然,你没有必要完全依照步骤去做,这个步骤是你对控件应该怎么写已经有了完整的思路和规划,这在实际情况下是不现实的,往往我们自定义控件都是做到哪里缺什么就做什么,首先我们应该将它画出来,有一个可视的供我们思考的视图。所以,这里我们将这个步骤灵活的变换一下,由于我们现在还不确定需要自定义哪些属性,以及需要怎样测量,所以我们把这两个步骤挪到后面。
一个供选择的数组
一些必要的数据:字体大小
直线的长度
圆圈的分布间隔距离
字体与上面部分的间距
在动手之前,我们要注意:直线的长度应该在控件完成测量后才能计算,所以应该在
代码:
布局文件:
运行效果:
重写onMeasure:
运行结果:
发现高度还是不对,其实这个地方并不是上面重写
对于参数y的说明中,它指的是baseline的y轴坐标,而不是文字top的y坐标,对于baseline,后面再做说明,所以,我们计算
再看看运行效果:
效果图:
布局中使用:
运行效果:
欢迎关注,希望在这里有你想要的,博主会持续更新高(di)质(ji)量(shu)的文章和大家交流学习,祝各位学习愉快。
喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢
http://download.csdn.net/detail/u010163442/9698879 CSDN下载平台太流氓
https://github.com/openXu/SlidingTab
http://blog.csdn.net/xmxkf/article/details/52178553
本文出自:【openXu的博客】
目录:
初步分析重写onDraw绘制
重写onMeasure计算宽高
重写onTouch加入滑动效果
自定义属性
源码下载
这篇博客我们来一发自定义控件的实战,恰好前些天有一个小需求,效果图如下:
根据效果图,我们可以确定,用自定义View完全可以搞定,在自定义控件系列博客第一篇中,我们总结了自定义View的几个步骤:
继承View,覆盖构造方法
自定义属性
重写onMeasure方法测量宽高
重写onDraw方法绘制控件
当然,你没有必要完全依照步骤去做,这个步骤是你对控件应该怎么写已经有了完整的思路和规划,这在实际情况下是不现实的,往往我们自定义控件都是做到哪里缺什么就做什么,首先我们应该将它画出来,有一个可视的供我们思考的视图。所以,这里我们将这个步骤灵活的变换一下,由于我们现在还不确定需要自定义哪些属性,以及需要怎样测量,所以我们把这两个步骤挪到后面。
1. 初步分析,重写onDraw绘制
首先我们分析一下这个控件里面有哪些元素,有一条直线,上面有n个选项,分布着n个圆,当选中哪一个后这上面的圆变为蓝色的,还有n项字,当选中后字变为蓝色。下面我们初步确定一下需要的常量和一些简单的计算:一个供选择的数组
String[] tabNames = new String[]{"tab1","tab2","tab3","tab4"}
一些必要的数据:字体大小
mTextSize,字体颜色
mColorTextDef,线段和圆圈的颜色
mColorDef,被选中后的颜色
mColorSelected,直线的高度
mLineHight,圆圈的直径
mCircleHight,被选中后蓝色空心圆圈的宽度
mCircleSelStroke,当前选中的序号
selectedIndex
直线的长度
float lineLength=整个控件的宽度-左边圆圈的半径 -右边圆圈的半径(为了让直线两端正好在两端圆圈的中心)
圆圈的分布间隔距离
float splitLength = lineLength / (n-1);
字体与上面部分的间距
mMarginTop
在动手之前,我们要注意:直线的长度应该在控件完成测量后才能计算,所以应该在
onMeasure中计算。现在我们可以动手了,首先继承
View,覆盖构造方法,然后重写
onDraw,在上面画出初步的轮廓。
代码:
public class SlideTab extends View { String TAG = "SlidingTab"; private int mTextSize; //文本的字体大小 private int mColorTextDef; // 默认文本的颜色 private int mColorDef; // 线段和圆圈颜色 private int mColorSelected; //选中的字体和圆圈颜色 private int mLineHight; //基准线高度 private int mCircleHight; //圆圈的高度(直径) private int mCircleSelStroke; //被选中圆圈(空心)的粗细 private int mMarginTop; //圆圈和文字之间的距离 private String[] tabNames; //需要绘制的文字 /** * 下面需要计算 */ private float splitLengh; //每一段横线长度 private int textStartY; //文本绘制的Y轴坐标 private List<Rect> mBounds; //保存文本的量的结果 private int selectedIndex = 0; //当前选中序号 private Paint mTextPaint; //绘制文字的画笔 private Paint mLinePaint; //绘制基准线的画笔 private Paint mCirclePaint; //绘制基准线上灰色圆圈的画笔 private Paint mCircleSelPaint; //绘制被选中位置的蓝色圆圈的画笔 public SlideTab(Context context) { this(context, null); } public SlideTab(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideTab(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //初始化属性 tabNames = new String[]{"tab1","tab2","tab3","tab4"}; mColorTextDef = Color.GRAY; mColorSelected = Color.BLUE; mColorDef = Color.argb(255,234,234,234); //#EAEAEA mTextSize = 20; mLineHight = 5; mCircleHight = 20; mCircleSelStroke = 10; mMarginTop = 50; mLinePaint = new Paint(); mCirclePaint = new Paint(); mTextPaint = new Paint(); mCircleSelPaint = new Paint(); mLinePaint.setColor(mColorDef); mLinePaint.setStyle(Paint.Style.FILL);//设置填充 mLinePaint.setStrokeWidth(mLineHight);//笔宽像素 mLinePaint.setAntiAlias(true);//锯齿不显示 mCirclePaint.setColor(mColorDef); mCirclePaint.setStyle(Paint.Style.FILL);//设置填充 mCirclePaint.setStrokeWidth(1);//笔宽像素 mCirclePaint.setAntiAlias(true);//锯齿不显示 mCircleSelPaint.setColor(mColorSelected); mCircleSelPaint.setStyle(Paint.Style.STROKE); //空心圆圈 mCircleSelPaint.setStrokeWidth(mCircleSelStroke); mCircleSelPaint.setAntiAlias(true); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(mColorTextDef); mLinePaint.setAntiAlias(true); measureText(); } /** * measure the text bounds by paint */ private void measureText(){ mBounds = new ArrayList<>(); for(String name : tabNames){ Rect mBound = new Rect(); mTextPaint.getTextBounds(name, 0, name.length(), mBound); mBounds.add(mBound); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); initConstant(); } private void initConstant(){ int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight; splitLengh = lineLengh/(tabNames.length-1); textStartY = mCircleHight + mMarginTop + getPaddingTop(); } @Override protected void onDraw(Canvas canvas) { //画灰色基准线 canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint); float centerY = mCircleHight/2; for(int i = 0; i<tabNames.length; i++){ float centerX = mCircleHight/2+(i*splitLengh); //float cx, float cy, float radius, @NonNull Paint paint //画基准线上灰色小圆圈 // Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY); canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint); mTextPaint.setColor(mColorTextDef); if(selectedIndex == i){ //画选中位置的蓝色圆圈 mCircleSelPaint.setStrokeWidth(mCircleSelStroke); mCircleSelPaint.setStyle(Paint.Style.STROKE); // Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY+" 半径:"+(mCircleHight-mCircleSelHight)/2); canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint); mTextPaint.setColor(mColorSelected); } //绘制文字 float startX; if(i == 0){ startX = 0; }else if(i == tabNames.length-1){ startX = getWidth()-mBounds.get(i).width(); }else{ startX = centerX-(mBounds.get(i).width()/2); } // Log.v(TAG, "写字:X:"+startX+" Y:"+textStartY +" 字宽度:"+mBounds.get(i).width()); canvas.drawText(tabNames[i], startX, textStartY, mTextPaint); } } }
布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dip"> <com.openxu.st.SlideTab android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#aaff0000"/> </LinearLayout>
运行效果:
2. 重写onMeasure计算宽高
基本的效果图已经出来了,不知道你们有没有发现,我在写布局文件的时候设置的高度是wrap_content,并且为控件设置了红色背景以便于参考,运行结果显示控件的高度却占满的整个屏幕,所以我们应该用重写
onMeasure测量控件的高度(不熟悉
onMeasure可以参照博客Android自定义View(三、深入解析控件测量onMeasure))。对于此控件,它的高度设置为填充父窗体,高度应该是圆圈的直径+字体的高度+字体与上面部分的距离。
重写onMeasure:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式 int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 int height ; if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { float textHeight = mBounds.get(0).height(); height = (int) (textHeight + mCircleHight + mMarginTop); // Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height); } //保存测量宽度和测量高度 setMeasuredDimension(widthSize, height); initConstant(); }
运行结果:
发现高度还是不对,其实这个地方并不是上面重写
onMeasure有问题,而是绘制文本的Y坐标的问题,我们看看
drawText方法的注释:
/** * Draw the text, with origin at (x,y), using the specified paint. The * origin is interpreted based on the Align setting in the paint. * * @param text The text to be drawn * @param x The x-coordinate of the origin of the text being drawn * @param y The y-coordinate of the baseline of the text being drawn * @param paint The paint used for the text (e.g. color, size, style) */ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); }
对于参数y的说明中,它指的是baseline的y轴坐标,而不是文字top的y坐标,对于baseline,后面再做说明,所以,我们计算
textStartY的时候,应该计算baseline的y坐标:
private void initConstant(){ int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight; splitLengh = lineLengh/(tabNames.length-1); // FontMetrics对象 Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); textStartY = getHeight() - (int)fontMetrics.bottom; //baseLine的位置 // textStartY = mCircleHight + mMarginTop + getPaddingTop(); }
再看看运行效果:
3. 重写onTouch加入滑动效果
现在,文字显示已经没有问题了,接下来,我们加入手指滑动的效果。此控件只支持左右滑动,手指滑动到某个位置的时候记录xy的坐标值,然后将蓝色选中的圆圈移动到x位置,其实就是在手指的位置画一个蓝色的圆圈,还要根据x的值计算当前偏向于选择哪一个标签。这里需要注意的地方是event.getX()和
event.getY()获取到的手指的坐标是相对于本控件左上角的坐标(本控件左上角为原点),具体看下面代码,注释已经很清楚了:
@Override protected void onDraw(Canvas canvas) { //画灰色基准线 canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint); float centerY = mCircleHight/2; for(int i = 0; i<tabNames.length; i++){ float centerX = mCircleHight/2+(i*splitLengh); //float cx, float cy, float radius, @NonNull Paint paint //画基准线上灰色小圆圈 // Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY); canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint); mTextPaint.setColor(mColorTextDef); if(selectedIndex == i){ if(!isSliding){ //画选中位置的蓝色圆圈 mCircleSelPaint.setStrokeWidth(mCircleSelStroke); mCircleSelPaint.setStyle(Paint.Style.STROKE); // Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY+" 半径:"+(mCircleHight-mCircleSelHight)/2); canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint); } mTextPaint.setColor(mColorSelected); } //绘制文字 float startX; if(i == 0){ startX = 0; }else if(i == tabNames.length-1){ startX = getWidth()-mBounds.get(i).width(); }else{ startX = centerX-(mBounds.get(i).width()/2); } // Log.v(TAG, "写字:X:"+startX+" Y:"+textStartY +" 字宽度:"+mBounds.get(i).width()); canvas.drawText(tabNames[i], startX, textStartY, mTextPaint); } //画手指拖动位置圆圈,最后画,避免被其他圆圈覆盖 if(isSliding){ // Log.v(TAG, "手指拖动画圆:X:"+slidX+" Y:"+centerY+" 半径:"+mCircleHight/2); mCircleSelPaint.setStrokeWidth(1); mCircleSelPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(slidX, centerY, mCircleHight/2, mCircleSelPaint); } } private boolean isSliding = false; //手指是否在拖动 private float slidX, slidY; //手指当前位置(相对于本控件左上角的坐标) @Override public boolean onTouchEvent(MotionEvent event) { slidX = event.getX(); //以本控件左上角为坐标原点 slidY = event.getY(); //左右越界 if(slidX< mCircleHight/2) slidX = mCircleHight/2; if(slidX>(getWidth() - mCircleHight/2)) slidX = getWidth() - mCircleHight/2; Log.e(TAG, "手指位置: getX:"+slidX+" getY:"+slidY); float select = slidX/splitLengh; int xs = (int)(select*10)-(((int)select)*10); selectedIndex = (int)select +(xs>5?1:0); // Log.w(TAG, "手指位置在第"+select+"位置,小数为:"+xs+" ,选中的序列为:"+selectedIndex); //TODO 如果要求手指脱离了直线所在矩形之后停止滑动,放开下面代码 /* if(slidY>mCircleHight || slidY < 0){ Log.e(TAG, "手指落在外面了"); if(isSliding){ //滑动到外面的,这时候需要重新绘制一次,其他事件不用重绘 isSliding = false; invalidate(); } isSliding = false; return super.onTouchEvent(event); }*/ switch (event.getAction()){ case MotionEvent.ACTION_DOWN: isSliding = true; // Log.e(TAG, "手指按下: getX:"+slidX+" getY:"+slidY); break; case MotionEvent.ACTION_MOVE: // Log.i(TAG, "手指滑动: getX:"+slidX+" getY:"+slidY); break; case MotionEvent.ACTION_UP: // Log.e(TAG, "手指抬起: getX:"+slidX+" getY:"+slidY); isSliding = false; break; } invalidate(); return true; }
效果图:
4. 自定义属性
目前为止,控件基本能够正常使用了,如果你认为这样就可以了,那就不用往下看了。这个样子使用起来很不方便,如果很多地方需要用到此控件,而且控件中的字体大小颜色等都不一样,那是不是得写很多这样的控件(只是改变一下里面一些常量的值)?所以为了让这个控件使用更加灵活,可以自定义一些属性,这样只需要在布局文件中设置属性值即可。自定义属性具体方法请参见(Android自定义View(二、深入解析自定义属性))。<?xml version="1.0" encoding="utf-8"?> <resources> <!-- private int mColorTextDef; // 默认文本的颜色 private int mColorDef; // 线段和圆圈颜色 private int mColorSelected; //选中的字体和圆圈颜色 private int mLineHight; //基准线高度 private int mCircleHight; //圆圈的高度(直径) private int mCircleSelStroke; //被选中圆圈(空心)的粗细 private int mMarginTop; //圆圈和文字之间的距离 private String[] tabNames; //需要绘制的文字 private int mTextSize; //文本的字体大小 --> <declare-styleable name="SlidTab"> <attr name="textColorDef" format="reference|color"/> <!--默认文本的颜色--> <attr name="android:textSize"/> <!--文本的字体大小--> <attr name="defColor" format="reference|color" /> <!--线段和圆圈颜色--> <attr name="selectedColor" format="reference|color" /><!--选中的字体和圆圈颜色--> <attr name="lintHight" format="dimension" /> <!--基准线高度--> <attr name="circleHight" format="dimension" /> <!--圆圈的高度(直径)--> <attr name="circleSelStroke" format="dimension" /> <!--被选中圆圈(空心)的粗细--> <attr name="mMarginTop" format="dimension" /> <!--圆圈和文字之间的距离--> <attr name="tabNames" format="reference" /> <!--需要绘制的文字--> </declare-styleable> </resources>
布局中使用:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:openXu="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dip"> <com.openxu.st.SlideTab android:id="@+id/slideTab" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize = "15sp" openXu:textColorDef = "#A4A4A4" openXu:defColor = "#EAEAEA" openXu:selectedColor = "#5CBB8C" openXu:lintHight = "2dip" openXu:circleHight = "20dip" openXu:circleSelStroke = "5dip" openXu:mMarginTop = "15dip" openXu:tabNames = "@array/tab_names" /> </LinearLayout>
运行效果:
欢迎关注,希望在这里有你想要的,博主会持续更新高(di)质(ji)量(shu)的文章和大家交流学习,祝各位学习愉快。
喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢
源码下载:
注:没有积分的童鞋 请留言索要代码喔http://download.csdn.net/detail/u010163442/9698879 CSDN下载平台太流氓
https://github.com/openXu/SlidingTab
相关文章推荐
- Android自定义View实战(SlideTab-可滑动的选择器)
- Android 自定义View结合自定义TabLayout实现顶部标签滑动效果
- Android实战简易教程<三十四>(基于ViewPager和FragmentPagerAdapter实现滑动通用Tab)
- 【Android】自定义View —— 滑动的次数选择器
- android 自定义ImageView实现图片手势滑动,多点触摸放大缩小效果
- android 自定义ImageView实现图片手势滑动,多点触摸放大缩小效果
- Android ActionBar+ViewPager 实现左右滑动Tab
- Android学习自定义View(四)——继承控件(滑动时ListView的Item出现删除按钮)
- 自定义的带tab的可左右滑动的viewpager之二viewpager与fragment不兼容
- android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu
- android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu
- (android实战)自定义android的tab样式--转载
- Android 自定义Viewpager滑动速度
- Android自定义View示例(三)—滑动控件
- Android开发学习之使用ViewPager+PagerTabStrip制作可滑动的Tab
- android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu
- (转)android 自定义ViewGroup和对view进行切图动画实现滑动菜单SlidingMenu
- android 自定义ImageView实现图片手势滑动,多点触摸放大缩小效果