前言:只有苦练七十二变,才能笑对八十一难,戏里如此,人生亦然。————六小龄童
相关文章:
1、《鱼眼索引控件详解之一 —— 自定义索引器》
2、《鱼眼索引控件详解之二 —— 快速索引实现》
有些同学,想要多我出些有关自定义控件的知识,我也就顺应民意吧,在总结其它知识的同时,夹杂着出些有关自定义控件的博客给大家。
我们先看看这个系列的最终效果图:
从这个效果图中,可以看到,通过这个系列我们将涉及下面几个方面的知识:
首先,创建一个工程BlogSideBar
然后创建一个派生自View的类:IndexSideBar
public class IndexSideBar extends View { public IndexSideBar(Context context, AttributeSet attrs) { super(context, attrs); } }非常简单,添加一个构造函数即可。然后将此控件加入到MyActivity的布局文件中:(main.xml)
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffff"> <com.example.BlogSideBar.IndexSideBar android:id="@+id/index_slide_bar" android:layout_width="23dp" android:layout_height="match_parent" android:layout_alignParentRight="true" android:layout_marginRight="10dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:background="@drawable/index_letter_bg"/> </RelativeLayout>在xml中,我们将IndexSideBar靠右排列,宽度设为23dp,高度为match_parent,边距为10dp;
如果现在运行一下,结果将是下面这样的除了拉伸的背景以外,其它都是一片空白:
这是因为,对于自定义的View,背景可以通过xml来添加,但其中的内容都是通过onDraw函数自己画上去的。由于我们还没有重写onDraw函数,当然除了背景以外的位置都是空白。
public static String[] b = {"#","A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V","W", "X", "Y", "Z"}; private List<String> indexContent = new ArrayList<>(); private Paint paint = new Paint(); public IndexSideBar(Context context, AttributeSet attrs) { super(context, attrs); initIndex(); } private void initIndex(){ for (String str:b){ indexContent.add(str); } } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int height = getHeight();// 获取对应高度 int width = getWidth(); // 获取对应宽度 int singleHeight = height / (indexContent.size() + 1);// 获取每一个字母的高度 //画圆圈 paint.setColor(Color.parseColor("#fa7829")); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(width / 2.0f, singleHeight/2, singleHeight/4, paint); paint.reset(); //写字 for (int i = 0; i < indexContent.size(); i++) { paint.setColor(Color.parseColor("#888888")); paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setAntiAlias(true); paint.setTextSize(22); paint.setTextAlign(Paint.Align.CENTER); //x源相对坐标设置为中间位置 float xPos = width / 2; //给点中间线的位置,计算出baseline位置 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int center = singleHeight * (i+1) + singleHeight/2; int baseline = center + (fm.bottom - fm.top)/2 - fm.bottom; canvas.drawText(indexContent.get(i), xPos, baseline, paint); paint.reset();// 重置画笔 } }这段代码看起来特别长,其实只是分为三部分:
public static String[] b = {"#","A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; private List<String> indexContent = new ArrayList<>(); private Paint paint = new Paint(); public IndexSideBar(Context context, AttributeSet attrs) { super(context, attrs); initIndex(); } private void initIndex(){ for (String str:b){ indexContent.add(str); } }这段代码,其实就是初始化两个变量,一个是paint,一个是盛装索引字符的List indexContent;在初始化List indexContent时,我们首先定义一个字符数组 String[] b,通过数组b来初始化indexContent。
先看最右侧的IndexSideBar:
int singleHeight = getHeight()/(indexContent.size()+1);然后再看左边的圆圈放大图,从图中可以看到:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); int height = getHeight();// 获取对应高度 int width = getWidth(); // 获取对应宽度 int singleHeight = height / (indexContent.size() + 1);// 获取每一个字母的高度 //画圆圈 paint.setColor(Color.parseColor("#fa7829")); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(width / 2.0f, singleHeight/2, singleHeight/4, paint); paint.reset(); }首先,得到单个字符的高度singleHeight:
int height = getHeight();// 获取对应高度 int width = getWidth(); // 获取对应宽度 int singleHeight = height / (indexContent.size() + 1);// 获取每一个字母的高度然后画出圆形:
paint.setColor(Color.parseColor("#fa7829")); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(width / 2.0f, singleHeight/2, singleHeight/4, paint); paint.reset();我们有了圆心位置,圆的半径以后,很容易就能画出圆形了,如果有对画圆函数不了解的同学,可以参考 《android Graphics(一):概述及基本几何图形绘制》
前面已经说过,我们将总高度getHeight()根据字符个数等分,每个字符所占的高度是singleHeight;
如上图所示,我们假设要得到字符C的基线位置。
首先,我们要得到字符C之前所有字符的总高度,即C之前的所有字符个数,即四个。所以C字符的所占位置起始位置是singleHeight * 4;但注意一点是,这个起始位置,可并不是我们字符开始画的位置,我们只知道,每个字符占的间距是singleHeight,但单个字符真正在singleHeight的什么位置开始画,我们是不知道的。但我们唯一能确定的是每个字符在singleHeight中都是居中的!这就对了,我们能到字符C的中间线的位置:
centerLine = singleHeight *4 + singleHeight/2;即字符C的起始高度 singleHeight *4再加上一半的singleHeight的高度就是字符C的中间线的位置。
baseline = centerLine + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;所以字符C的中间线也就出来了:
centerLine = singleHeight *4 + singleHeight/2; baseline = centerLine + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;好了,我们现在看写文字的代码:
//写字 for (int i = 0; i < indexContent.size(); i++) { paint.setColor(Color.parseColor("#888888")); paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setAntiAlias(true); paint.setTextSize(22); // x坐标等于中间-字符串宽度的一半. float xPos = width / 2 - paint.measureText(indexContent.get(i)) / 2; //给点中间线的位置,计算出baseline位置 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int center = singleHeight * (i+1) + singleHeight/2; int baseline = center + (fm.bottom - fm.top)/2 - fm.bottom; canvas.drawText(indexContent.get(i), xPos, baseline, paint); paint.reset();// 重置画笔 }这段代码是的第一部分,就是设置画笔:
paint.setColor(Color.parseColor("#888888")); paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setAntiAlias(true); paint.setTextSize(22); paint.setTextAlign(Paint.Align.CENTER); // x源相对坐标设置为中间位置 float xPos = width / 2;在这段代码里,最重要的是注意这两句:
paint.setTextAlign(Paint.Align.CENTER); // x源绘制坐标设置为中间位置 float xPos = width / 2;根据 《android Graphics( 五):drawText()详解》可知,将相对位置设置为控件中间位置的中间,那么这个文字将会在控件正中间绘制出来。理解不了的同学,请移步看看这个篇文章,这里就不再重讲一遍了。
Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int center = singleHeight * (i+1) + singleHeight/2; int baseline = center + (fm.bottom - fm.top)/2 - fm.bottom;这些就是利用我们前面讲过的原理,先得到中间线center的位置,然后再利用中间线求基线位置的公式得到基线位置。
canvas.drawText(indexContent.get(i), xPos, baseline, paint);好了,到这里,我们就在IndexSideBar上画出了所有的字符了。下面我们就是要捕捉点击事件,在用户点击某个字符的时候,将该字符画为选中状态。
从效果图中可以看到在选中一个字母时,我们做了两件事:
private int mChoose = -1; public boolean onTouchEvent(MotionEvent event) { final float y = event.getY();// 点击y坐标 // 点击y坐标所占总高度的比例*b数组的长度就等于点击b中的个数. int pos = (int)(y / getHeight() * (indexContent.size()+1)); //由于总共的字符中包含最顶层的圆圈,所以indexContent中真正字符的位置应当在当前mChoose位置上减一 mChoose = pos -1 ; switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:{ mChoose = -1; } break; default: break; } //强制重绘 invalidate(); return true; }首先定义一个成员变量来标识当前选中字母的索引:
private int mChoose = -1;然后是通过当前手指位置来计算当前选中是哪个字母:
final float y = event.getY();// 点击y坐标 // 点击y坐标所占总高度的比例*b数组的长度就等于点击b中的个数. int pos = (int)(y / getHeight() * (indexContent.size()+1)); //由于总共的字符中包含最顶层的圆圈,所以indexContent中真正字符的位置应当在当前pos位置上减一 mChoose = pos -1 ;在这段代码中, 我们通过event.getY()来获得用户当前手指的位置。
switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:{ mChoose = -1; } break; default: break; } //强制重绘 invalidate();当用户手指抬起的时候,将mChoose设置为-1,表示当前没有选中任何值。
return true;最后在return的时候,一定要return true!不然你会发现,只有点击的时候那一下会走到OnTouchEvent()以后的ACTION_MOVE和ACTION_UP都不会再走到OnTouchEvent()中了。
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 获取焦点改变背景颜色. int height = getHeight();// 获取对应高度 int width = getWidth(); // 获取对应宽度 int singleHeight = height / (indexContent.size() + 1);// 获取每一个字母的高度 //画圆圈 if (-1 == mChoose){ paint.setColor(Color.parseColor("#888888")); }else { paint.setColor(Color.parseColor("#fa7829")); } paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); canvas.drawCircle(width / 2.0f, singleHeight / 2, singleHeight / 4, paint); paint.reset(); //写字 for (int i = 0; i < indexContent.size(); i++) { if (i == mChoose){ paint.setColor(Color.parseColor("#fa7829")); }else { paint.setColor(Color.parseColor("#888888")); } paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setAntiAlias(true); paint.setTextSize(22); paint.setTextAlign(Paint.Align.CENTER); //x源相对坐标设置为中间位置 float xPos = width / 2; //给点中间线的位置,计算出baseline位置 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int center = singleHeight * (i + 1) + singleHeight / 2; int baseline = center + (fm.bottom - fm.top) / 2 - fm.bottom; canvas.drawText(indexContent.get(i), xPos, baseline, paint); paint.reset();// 重置画笔 } }在这里,我们做在两个地方做了改变:
//画圆圈 if (-1 == mChoose){ paint.setColor(Color.parseColor("#888888")); }else { paint.setColor(Color.parseColor("#fa7829")); } paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); canvas.drawCircle(width / 2.0f, singleHeight / 2, singleHeight / 4, paint);在画圆圈时,根据当前mChoose是否等于-1,即是否有选中字符来设置圆圈的填充色。
for (int i = 0; i < indexContent.size(); i++) { if (i == mChoose){ paint.setColor(Color.parseColor("#fa7829")); }else { paint.setColor(Color.parseColor("#888888")); } paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setAntiAlias(true); paint.setTextSize(22); paint.setTextAlign(Paint.Align.CENTER); ………… }到这里,设置选中字体字体颜色的代码也就结束了。下面我们就来看看在选中一个字符时,使用放大器将基显示出来是如何做出来的。
从效果图中可以看出来,这里我们要实现两个效果:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffff"> <com.example.BlogSideBar.IndexSideBar android:id="@+id/index_slide_bar" android:layout_width="23dp" android:layout_height="match_parent" android:layout_alignParentRight="true" android:layout_marginRight="10dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:background="@drawable/index_letter_bg"/> <TextView android:id="@+id/index_slide_dialog" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@+id/index_slide_bar" android:background="@drawable/list_lable_screen" android:gravity="center" android:textColor="#ff5e00" android:textSize="30dp" android:visibility="invisible" android:layout_marginRight="6dp"/> </RelativeLayout>在这里没什么是注意的,就是给放大器放了一个背景:
其它没什么讲的了。下面让我们看看代码
IndexSideBar
我们先看看完整添加的代码:
/** * 设置放大器 * @param tv textview */ public void setIndicatorTv(TextView tv){ mIndicatorTv = tv; } public boolean onTouchEvent(MotionEvent event) { final float y = event.getY();// 点击y坐标 int pos = (int)(y / getHeight() * (indexContent.size()+1)); mChoose = pos -1 ; String text = indexContent.get(mChoose); if (null != mIndicatorTv) { mIndicatorTv.setVisibility(VISIBLE); mIndicatorTv.setText(text); } switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:{ mChoose = -1; if (null != mIndicatorTv) { mIndicatorTv.setVisibility(GONE); } } break; default: break; } return true; }这里的代码总共分为三部分:设置放大器对应TextView,设置选中字符,手指离开时隐藏放大器
public void setIndicatorTv(TextView tv){ mIndicatorTv = tv; }然后,当手指选中一个字符时,我们需要将其设置给放大器:
String text = indexContent.get(mChoose); if (null != mIndicatorTv) { mIndicatorTv.setVisibility(VISIBLE); mIndicatorTv.setText(text); }最后,当手指离开时,需要将放大器隐藏:
switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:{ mChoose = -1; if (null != mIndicatorTv) { mIndicatorTv.setVisibility(GONE); } } break; default: break; }最后是在MyActivity中设置放大器对应的TextView:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mIndexSideBar = (IndexSideBar)findViewById(R.id.index_slide_bar); mIndexBlockDialog = (TextView)findViewById(R.id.index_slide_dialog); mIndexSideBar.setIndicatorTv(mIndexBlockDialog); }到这里放大器的设置与使用就讲完了,下面就讲讲如何将选中的字符外露给使用者吧。
public static interface ChooseListner{ void onChoosed(int pos,String text); } /** * 设置监听器 * @param listener */ public void setChoosedListener(ChooseListner listener){ mListener = listener; }在这里,我们定义了一个接口,并且定义了一个函数setChoosedListener,让使用者传进来监听器的实例。
public boolean onTouchEvent(MotionEvent event) { final float y = event.getY();// 点击y坐标 int pos = (int)(y / getHeight() * (indexContent.size()+1)); mChoose = pos -1 ; String text = indexContent.get(mChoose); if (null != mIndicatorTv) { mIndicatorTv.setVisibility(VISIBLE); mIndicatorTv.setText(text); } if (null != mListener){ mListener.onChoosed(mChoose,text); } switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:{ mChoose = -1; if (null != mIndicatorTv) { mIndicatorTv.setVisibility(GONE); } } break; default: break; } invalidate(); //一定要返回true,因为,只有拦截了down事件以后,其它的事件才会再次传到这个控件来, // 不然就再也不会传到这个控件里来 return true; }最后是在MainActivity中使用监听器了:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mIndexSideBar = (IndexSideBar)findViewById(R.id.index_slide_bar); mIndexBlockDialog = (TextView)findViewById(R.id.index_slide_dialog); mIndexSideBar.setIndicatorTv(mIndexBlockDialog); mIndexSideBar.setChoosedListener(new IndexSideBar.ChooseListner() { @Override public void onChoosed(int pos,String text) { Log.e("qijian","pos:"+pos+" choosed:"+ text); } }); }结果如下:
好了,这篇文章就结束了,下面给大家讲讲实现快速索引的雏形,在这篇文章的底部给大家稍微讲一下有关九宫格拉伸区域与显示区域的知识。
标记A和B的两个点色线条(这里是两个点),是填充线条,标记A表示当横向拉伸时,用来填充拉伸位置的颜色源。同理,标记B表示当纵向拉伸时,拉伸处的颜色也是从标记B中取得的。
标记C和标记D就有点难理解了,他们表示的是内容的显示位置。
如标记C:
红色框标识的位置就是内容在纵向位置的显示区域。
显示区域的意思就是指,显示内容的区域!如果这是一个TextView控件的背景,那么TextView中的文字只能在C标识的区域显示,C区域以上和以下的区域都不能显示文字。
当拉伸时,显示区域是怎样的呢?如下图:
这张图显示的是纵向拉伸后的结果,在纵向拉伸后,我们用1,2,3标识了三个区域,其中区域1是未拉伸时,红色框上部的部分,区域3标识在未拉伸时红色框下部的部分;区域2表示拉伸后,.9图中C黑色块所对应的显示区域。
对于横向拉伸而言:
只有中间黑色块的拉伸区域才能用来显示内容,而区域A和区域B是不能显示内容的。同样,如果这是一个TextView控件的背景,那么文字就是在黑色框区域显示的。
那么问题来了,对于同时横向拉伸和纵向拉伸的图像,那显示区域是哪些呢?
当然是在横向显示区域与纵向显示区域的交集。
**但是注意一点:点九图的显示区域对于系统控件才有用,对于自定义控件无效!!!!!因为,我们利用绘图函数在给图时,整个区域都是可以画的,我们想画哪里画哪里,没有任何限制!**
如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9390218
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/50458830 谢谢