自己动手做sidebar

时间:2021-03-14 13:36:52

由于项目原因,需要一个类似联系人列表那种选择的“导航”,这玩意叫什么名, 我至今还是不太清楚, 听群里有哥们说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