自己动手做sidebar

时间:2022-06-16 13:37:48

由于项目原因,需要一个类似联系人列表那种选择的“导航”,这玩意叫什么名, 我至今还是不太清楚, 听群里有哥们说this is sidebar, 那咱们也叫他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+当前内容高度之间”。 下面我们来画图说明一下:

自己动手做sidebar



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显示的内容。 

最后,我们来看看整体效果

自己动手做sidebar