由于项目原因,需要一个类似联系人列表那种选择的“导航”,这玩意叫什么名, 我至今还是不太清楚, 听群里有哥们说this is sidebar, 那咱们也叫他sidebar吧。首先来一张图片, 来看看sidebar到底是个什么玩意。
ok, 就这玩意, 大家应该很熟悉吧, 这篇博客我们就来做这么一个东西,首先说明一点:代码~ so easy。
首先,来分析一下,当我们看到这个效果后,应该怎么去思考吧。第一眼看去,这玩意并不能实现,只要extends view, 然后measure,然后draw就可以。再看第二眼,应该去思考,如何判断当前滑动点对应的文字。ok, 为了提高灵活性,我们还需要把文字颜色、行间距、文本列表内容提取出来,让使用者可以在xml中配置使用。
1、编写attrs.xml文件
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="KeyPad"> <attr name="color" format="color|reference" /> <attr name="padding" format="dimension|reference" /> <attr name="android:textSize" /> <attr name="android:entries" /> </declare-styleable> </resources>color代表了字体的颜色,padding代表着行间距,android:textSize代码文字大小,android:entries代表列表内容,这里面需要注意的就是后两个以android:开头的,表示,直接使用android本身提供好的。
2、创建KeyPad.java文件
这里的KeyPad就是我们的SideBar啦, 刚开始写的时候不知道这玩意叫什么名,so...
public class KeyPad extends View { private String[] mKeys = { "#", "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 TextPaint mTextPaint; private Rect mTextBounds[]; private float mPadding; private String mSelected; private OnKeySelectedListener mListener; ... public void setEntries(String[] entries) { if (entries == null || entries.length == 0) return; mKeys = entries; mTextBounds = new Rect[mKeys.length]; requestLayout(); invalidate(); } public void setOnKeySelectedListener(OnKeySelectedListener l) { mListener = l; } public interface OnKeySelectedListener { public void onSelected(String key); public void onDown(); public void onUp(); } }
mKeys就是我们需要显示的列表,mSelected代表当前选中的文本,最后还有一个OnKeySelectedListener,有三个方法,onSelected表示有文本选中时,onDown和onUp表示按下和抬起时。KeyPad的基本框架就是这样,接下来的内容就是重写view那些步骤了, 获取属性、测量、绘制。。。
3、获取属性值
在第一步中,我们提供了几个属性,这几个属性可以在xml中进行配置,然后我们的KeyPad在构造方法中进行获取,来看看获取的代码。
public KeyPad(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.KeyPad, defStyle, 0); mPadding = ta.getDimension(R.styleable.KeyPad_padding, 20.f); int color = ta.getColor(R.styleable.KeyPad_color, Color.BLUE); float defaultTextSize = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 15, getResources().getDisplayMetrics()); float textSize = ta.getDimension(R.styleable.KeyPad_android_textSize, defaultTextSize); if (ta.hasValue(R.styleable.KeyPad_android_entries)) { mKeys = context.getResources().getStringArray( ta.getResourceId(R.styleable.KeyPad_android_entries, 0)); } ta.recycle(); ... }
这里仅仅需要注意的就是那个if语句,我们需要判断使用者是否在xml中通过android:entries="@array/xxx"配置了内容,如果配置了,则去获取它, 否则,使用默认。
4、view的测量
在KeyPad中,高度是最重要的,我们需要精确的测量KeyPad的高度,包括所有文本的高度和以及文本的行间距。上代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0; int height = 0; int length = mKeys.length; for (int i = 0; i < length; i++) { mTextBounds[i] = new Rect(); mTextPaint.getTextBounds(mKeys[i], 0, mKeys[i].length(), mTextBounds[i]); width = Math.max(width, mTextBounds[i].width()); height += mTextBounds[i].height() + mPadding; } height -= mPadding; width += getPaddingRight() + getPaddingLeft(); height += getPaddingTop() + getPaddingBottom(); setMeasuredDimension(width + 10, height); }
我们去遍历所有的文本,在循环中顺便初始化保存每个文本宽高信息的Rect,然后通过mTextPaint.getTextBounds去获取需要的东西,接下来的Math.max,能保证,我们的KeyPad的宽度是所有文本中最宽的那个的宽度,height就是一个累加的过程。纵观代码,这个view的测量还是很容易的。最后通过setMeasuredDimension保存一下测量值。
5、文本内容的绘制
KeyPad的大小我们已经测量完毕了,那么接下来的任务就是去绘制内容了, KeyPad的绘制部分也很容易。
@Override protected void onDraw(Canvas canvas) { int length = mKeys.length; int center = getMeasuredWidth() / 2; int height = 0; for (int i = 0; i < length; i++) { height += mTextBounds[i].height(); canvas.drawText(mKeys[i], center - mTextBounds[i].width() / 2 + getPaddingLeft(), height, mTextPaint); height += mPadding; } }
不多说了, 就是一行行的去绘制内容,注意,我们绘制的内容在水平方向上都是居中的。
6、重点:获取当前touch的内容
我们如何知道,当前touch的点对应哪一块内容呢? 我的思路是:获取当前touch的y坐标, 然后遍历每个文本的位置,进行比对,如果当前touch的y左边在某个文本坐标的区域内,则表示,touch到了该内容。先来看看代码是怎么实现的吧。
@Override public boolean dispatchTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { mSelected = null; if (mListener != null) mListener.onUp(); return true; } if (event.getAction() == MotionEvent.ACTION_DOWN) { if (mListener != null) mListener.onDown(); } int y = (int) event.getY(); int length = mKeys.length; int height = getPaddingTop(); for (int i = 0; i < length; i++) { if (y >= height && y <= height + mTextBounds[i].height() + mPadding) { if (mKeys[i].equals(mSelected)) break; mSelected = mKeys[i]; if (mListener != null) mListener.onSelected(mSelected); break; } height += mTextBounds[i].height() + mPadding; } return true; }
我们选择重写dispatchTouchEvent来处理touch。
首先来看看两个if语句,第一个if语句是判断如果是抬起的时候,这个时候,我们清空保存的选择项,并且回调onUp()以便使用者在抬起的时候做点什么。
第二个if语句是按下的时候,这里很简单, 只是单纯的回调了onDown()。
下面的代码是重点:
int y = (int) event.getY(); int length = mKeys.length; int height = getPaddingTop(); for (int i = 0; i < length; i++) { if (y >= height && y <= height + mTextBounds[i].height() + mPadding) { if (mKeys[i].equals(mSelected)) break; mSelected = mKeys[i]; if (mListener != null) mListener.onSelected(mSelected); break; } height += mTextBounds[i].height() + mPadding; }
首先获取到当前触摸的y坐标,然后,第4行,一个for循环,我们去遍历所有的内容,用一个if语句去判断当前触摸的点是否在这个内容所在的区域内,如果在,还有一个判断,if(mKey[i].equals(mSelected)) break主要是防止多次回调同一个内容,接下来,保存当前touch的内容并回调。
重点我们来看看if的条件是怎么判断的。
if (y >= height && y <= height + mTextBounds[i].height() + mPadding) -- height保存的是一个累加的高度,所以第一个条件很容易理解了,第二个条件是当y小于当前累加高度+当前文本的高度时。这个if的意思也就是“当y的位置在height和height+当前内容高度之间”。 下面我们来画图说明一下:
7、开始布局xml文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="org.loader.keypad.MainActivity" > <TextView android:id="@+id/tv_key" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="#AA888888" android:padding="50dp" android:textColor="@android:color/white" android:visibility="gone" /> <org.loader.keypad.KeyPad xmlns:pad="http://schemas.android.com/apk/res/org.loader.keypad" android:id="@+id/kp_keys" android:background="@android:color/darker_gray" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginRight="5dp" android:layout_marginTop="30dp" android:textSize="15sp" android:entries="@array/keys" pad:padding="10dp" pad:color="#FF0000FF" /> </RelativeLayout>
一个TextView, 用来显示当我们选中时的那个文本,重点来看看KeyPad的配置。 android:textSize="15sp"指定文本的大小,android:entries="@array/keys"指定数据集(就是一个string-array),pad:padding="10dp"指定文本的大小,pad:color="#FF0000FF"指定文本的颜色。
8、在Activity获取touch的内容并显示
public class MainActivity extends Activity { private TextView mKeyTextView; private KeyPad mKeyPad; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mKeyTextView = (TextView) findViewById(R.id.tv_key); mKeyPad = (KeyPad) findViewById(R.id.kp_keys); mKeyPad.setOnKeySelectedListener(new OnKeySelectedListener() { @Override public void onSelected(String key) { mKeyTextView.setText(key); } @Override public void onDown() { mKeyTextView.setVisibility(View.VISIBLE); } @Override public void onUp() { mKeyTextView.setVisibility(View.GONE); } }); // mKeyPad.setEntries(new String[] {"aaa","bbb"}); } }
主要看看设置的Listener里面,在onDown的时候我们让TextView显示出来, 在onUp我们又让TextView隐藏起来,在onSelected中控制TextView显示的内容。
最后,我们来看看整体效果