Android自定义View(一)

Android自定义View(一)
最近在做一个项目的room定制(主要负责Contacts模块),该定制选择的平台是Android 8.0 Oreo,8.0系统新特性这里就不说了,网上随便查找下会有一大批的资料,那么8.0 Contacts的变化呢?7.0之前的ContactsCommon从此和我们挥手告别了,仅有Contacts一个目录,当然该处需要除了Dialer等其他模块的继续使用。扯远了。。。回到我们今天的主题咯,由于8.0 Contacts目录结构变化比较大及该项目客制化的需要,我们要做很多自定义view,当然,如果经常使用自定义view的伙伴,那肯定早已熟知自定义view的强大的。
今天主要结合最近自己项目做的功能来简单说明自定义View的使用,当然仅是简单的介绍,后面会根据自己的学习及项目继续为大家呈现更深入自定义View效果及功能。
首先,我认为自定义View可以分为三种方式:组合控件,自绘控件和继承控件,其中自绘控件是今天的主角咯,其他两个就稍微提及略过了。
那我们开始吧???
1、组合控件
组合控件:就是将一些小的控件组合起来形成一个新的控件,这些小的控件多是系统自带的控件。比如很多应用中普遍使用的标题栏控件,其实用的就是组合控件,那么下面将通过实现一个简单的标题栏自定义控件来说说组合控件的用法;

a. 自定义标题栏的布局文件title_bar.xml:



b. TitleView 类,继承自RelativeLayout:
public class TitleView extends RelativeLayout {private Button mLeftBtn; private TextView mTitleTv; public TitleView(Context context, AttributeSet attrs) { super(context, attrs); // load layout LayoutInflater.from(context).inflate(R.layout.title_bar, this); // get layout mLeftBtn = (Button) findViewById(R.id.left_btn); mTitleTv = (TextView) findViewById(R.id.title_tv); }// set listener public void setLeftButtonListener(OnClickListener listener) { mLeftBtn.setOnClickListener(listener); }// set method public void setTitleText(String title) { mTitleTv.setText(title); } }

上面两步已经基本实现了组合控件的定义,接下来简单使用该组合控件; c. activity_main.xml中引入自定义的标题栏,类似如下:


d. MainActivity中获取自定义的标题栏,并且为返回按钮添加自定义点击事件:
private TitleView mTitleBar; mTitleBar = (TitleView) findViewById(R.id.title_bar); mTitleBar.setLeftButtonListener(new OnClickListener() { @Override public void onClick(View v) { //implements your action } }); mTitleBar.setTitleText("Your title text");

至此,组合控件的定义及使用已经OK了。效果图就不贴了,左边一个返回按钮,右侧一个Title子串。当然,组合控件TitleView中,你可以根据需要定义更多的方法,这样不断扩展组合控件的动能(哈哈,顿时此刻是不是有使用到了哪一个设计模式和设计原则呢,当然,这个需要自身体会咯)。 2、自绘控件
那么,欢迎主角登场了。[此处少了欢呼][此处少了鼓掌][此处少了呐喊][少了特多。。。]
在开始前先总结下自定义View的步骤:
a. 自定义View的属性
b. 在View的构造方法中获得我们自定义的属性
c. 重写onMesure
d. 重写onDraw
下面开始逐步举例说明;
a. 自定义View的属性,在res/values/attrs.xml里面定义我们的属性和声明我们的整个样式。



我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型,该类型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag;
然后在布局中声明我们的自定义View,注意要引入 命名空间:xmlns:custom="http://schemas.android.com/apk/res/com.example.customview",及包路径,包路径指的是项目的package;

那么自定义View属性及使用就结束了。 b. 在View的构造方法中,获得我们的自定义的样式

private Rect mBound; private Paint mPaint; public CustomTitleView(Context context, AttributeSet attrs) { this(context, attrs, 0); }public CustomTitleView(Context context) { this(context, null); } private String mTitleText; private int mTitleTextColor; private int mTitleTextSize; /** * @param context * @param attrs * @param defStyle */ public CustomTitleView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.CustomTitleView_titleText: mTitleText = a.getString(attr); break; case R.styleable.CustomTitleView_titleTextColor: mTitleTextColor = a.getColor(attr, Color.BLACK); break; case R.styleable.CustomTitleView_titleTextSize: // the defaule value is 16sp,transform the TypeValue sp to px mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); break; }} a.recycle(); /** * paint the view according to the size/color/text */ mPaint = new Paint(); mPaint.setTextSize(mTitleTextSize); mPaint.setColor(mTitleTextColor); mBound = new Rect(); mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound); }

自定义View属性及使用就OK了,回过头来看自定义的属性,其实这些属性完全可以使用系统原有的属性,自定义的无疑将是累赘的一面咯,当然,这个根据每个人使用及具体要求了。
c. 重写onMesure
onMesure方法不一定是必须的,可以直接调用父类的onMesure方法即可,当然,大部分情况下还是需要重写的,那么这个方法到底是干嘛的呢?这两个参数是父布局给它提供的水平和垂直的空间要求,onMeasure方法的作用就是计算出自定义View的宽度和高度,这个计算的过程参照父布局给出的大小,以及自己特点算出结果。
注意:系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,即当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法。
MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用


@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }

d. 重写onDraw

@Override protected void onDraw(Canvas canvas) { mPaint.setColor(Color.YELLOW); canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); mPaint.setColor(mTitleTextColor); canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); }

到了第四步,自定义的view就绘制OK了。

结合上面步骤,下面给出自己在8.0 contacts中自定义 右侧A~Z定位栏的布局及使用。
主要分为三个部分:xml文件具体定义、自定义view及方法实现,直接上代码,根据代码慢慢说来。当然,这里重点还是主讲自定义view相关,关于8.0 contacts相关架构逻辑不会有太详细的介绍咯,将是简单粗暴的略过。。。
a. 修改contact_list_content.xml布局,将自定义view ContactsBladeView添加至联系人列表最右侧位置(啰嗦下,联系人列表也是自定义View PinnedHeaderListView,需要调整contact_list_content布局,使列表PinnedHeaderListView和定位栏ContactsBladeView呈现水平排布,即需要添加一个LinearLayout将两者包裹起来,添加android:orientation="horizontal"属性,又扯远了。。。)


b. 自定义 ContactsBladeView实现,这里将是主要内容,通过下面code内容不断讲述咯,为了更方便理解,下面code里的注释将改成中文滴咯,不要太嫌弃哈,毕竟汉子看起来还是舒服点,但是建议一点,开发中所有注释最好都使用英文,说的高大上点,就是开发要专业点,说的实在点,就是避免code 编码等一系列问题咯,毕竟程序是人家英文字母创始滴啊。
package com.android.contacts.list; import android.content.ContentValues; import android.content.res.Configuration; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupWindow; import android.widget.TextView; import com.android.contacts.HanziToPinyin; import com.android.contacts.R; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; public class ContactsBladeView extends View { private String TAG = "HQContactsBladeView"; private final static boolean DEBUG_FLAG = false; private Context mContext; private int mCharWidth = 0; private int mCharHeight = 0; private int mChooseCharWidth = 0; private int mChooseCharHeight = 0; private int mChoose = -1; private final int DISMISS_CHAR_POPUP_AFTER_THREE_SECOND = 9; private final static int MAX_CHOOSE_CHAR_LIST = 9; private final static int MIM_CONTACTS_LIST = 6; private final static String NINTH_STRING = "..."; //最右侧A~Z字母导航字符 private final String[] mAlphabet = {/*"☆",*/ "★", "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 PopupWindow mPopupWindow; private TextView mPopupText; private PopupWindow mCharPopupWindow; private OnItemClickListener mOnItemClickListener; //联系人列表数据备份 private List chooseCharInfo = new ArrayList(); //选择字符查询到的联系人检索,该列表最长为MAX_CHOOSE_CHAR_LIST,即9个索引 private ArrayList mChooseCharList = new ArrayList(); public ContactsBladeView(Context context) { this(context, null); this.mContext = context; }public ContactsBladeView(Context context, AttributeSet attrs) { this(context, attrs, 0); this.mContext = context; }public ContactsBladeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; }public Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { //延时3秒取消选择子串的显示 case DISMISS_CHAR_POPUP_AFTER_THREE_SECOND: dismissCharPopup(); break; default: break; } } }; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //获取父布局给予的宽度和高度 int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //下面这两个需要根据具体分辨率来调试,一定需要赋值,上面已经说过,否则会达不到自己需要的效果 //设置右侧定位栏字符宽度 mCharWidth = widthSize + getPaddingLeft() + getPaddingRight(); //设置右侧定位栏字符高度,总高度/总的字符数 mCharHeight = heightSize / mAlphabet.length; setMeasuredDimension(widthSize, heightSize); }@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int textSize = 28; //35 //构造一个画笔,设置画笔相关属性,大部分一看应该即可明白,单独特殊的将会添加详细注释 Paint paint = new Paint(); /** *设置字体样式,有以下属性 *Typeface.DEFAULT:默认字体 *Typeface.DEFAULT_BOLD:加粗字体 *Typeface.MONOSPACE:monospace字体 *Typeface.SANS_SERIF:sans字体 *Typeface.SERIF:serif字体 */ paint.setTypeface(Typeface.DEFAULT_BOLD); //设置抗锯齿 paint.setAntiAlias(true); paint.setTextAlign(Paint.Align.CENTER); paint.setTextSize(textSize); //遍历28个字母,按顺序绘制出来 for (int i = 0; i < mAlphabet.length; ++i) { String currentChar = getCharString(i); if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { if (i % 2 == 0) { currentChar = "."; } textSize = (mCharHeight - 2) * 2; paint.setTextSize(textSize); } paint.setColor(getResources().getColor(R.color.hqbladeview_alphabet_color)); if (mChoose == i) { paint.setColor(android.graphics.Color.BLUE/*R.color.choose_alphabet_color*/); //paint.setFakeBoldText(true); //画笔加粗 } //绘制具体每个字符的X,Y坐标,这个应该可以理解吧,没理解,深呼吸,在体会下。。。 //仔细体会下Y坐标的意思,有没有一种移动换行的赶脚。。。 int textX = mCharWidth / 2 - textSize / 2; int textY = (i + 1) * mCharHeight; //根据位置,将28个字符不断绘制出来 canvas.drawText(currentChar, textX, textY, paint); } //绘制完成清理下画笔 paint.reset(); }/** * 相关事件的监听 * * @param event The motion event to be dispatched. * @return */ @Override public boolean dispatchTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); getParent().requestDisallowInterceptTouchEvent(true); //后去点击字符位置 int item = (int) (event.getY() / mCharHeight); if (item < 0 || item >= mAlphabet.length) { return true; } switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: showPopup(item); alphabetItemClicked(item); mChoose = item; break; case MotionEvent.ACTION_UP: dismissPopup(); showCharPopupListView(item); break; default: mChoose = -1; dismissPopup(); dismissCharPopup(); break; } //重新绘制画面 invalidate(); return true; }/** * 点击右侧字符后,界面中间使用popup显示被选中的字符 * * @param item */ private void showPopup(int item) { //这个就不用多说了吧,popup内显示字符的TextView if (mPopupText == null) { mPopupText = new TextView(getContext()); } String textChar = getCharString(item); mPopupText.setText(textChar); mPopupText.setTextSize(40); mPopupText.setTextColor(android.graphics.Color.WHITE/*R.color.choose_alphabet_popup_color*/); mPopupText.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL); //实例化一个popup if (mPopupWindow == null) { mPopupWindow = new PopupWindow(180, 180); } //将显示字符的TextView放置在popup里 mPopupWindow.setContentView(mPopupText); mPopupWindow.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.letter_background)); //前面说到的奥,联系人列表至少超过6人在popup中显示被选中的字符 if (!chooseCharInfo.isEmpty() && chooseCharInfo.size() > MIM_CONTACTS_LIST) { if (mPopupWindow.isShowing()) { mPopupWindow.update(); } else { //设置popup显示的位置 //第一个参数是View类型的parent,官方文档“a parent view to get the android.view.View.getWindowToken() token from”, //这个parent的作用应该是调用其getWindowToken()方法获取窗口的Token,所以,只要是该窗口(父类窗口)上的控件(例如:按钮控件)就可以了。 //第二个参数是Gravity,(Gravity.CENTER)可以使用|附加多个属性,如Gravity.LEFT|Gravity.BOTTOM。 //第三四个参数是x,y偏移。 mPopupWindow.showAtLocation(getRootView(), Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL, 0, 0); } } else {// hide the visible mPopupWindow.dismiss(); } }/** * 当手指抬起时,使用一个popuplist显示被选中字符对应联系人的相关索引 * * @param item */ private void showCharPopupListView(int item) { if (0 == item) {//remove "☆" String return; } //根据手指最后离开位置对应的字符获取相关联系人索引列表 setChooseCharList(item); log("showCharPopupListView list.size:" + mChooseCharList.size() + ",item:" + item); if (mChooseCharList.isEmpty()) { return; }ArrayList list = getListinfo(mChooseCharList); //设置联系人索引字符宽度和高度 mChooseCharWidth = 70; mChooseCharHeight = (mChooseCharWidth + 3) * list.size(); //使用一个listview显示索引列表 View view = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.choose_char_layout, null); ListView charListView = view.findViewById(R.id.lv_choose_char); //隐藏掉索引列表的滚动条 charListView.setVerticalScrollBarEnabled(false); charListView.setScrollbarFadingEnabled(false); charListView.setScrollingCacheEnabled(false); //使用自定义Adapter填充该listview,当然,这里可以使用系统ArrayAdapter及系统布局代替咯, //自定义的原因大家应该也很明白,就是扩展咯,是自己的adapter更加给力强大 charListView.setAdapter(new ChooseCharAdapter(mContext, R.layout.char_item, R.id.tv_char_item, list)); //类似的,实例化一个popup,用来放置索引listview mCharPopupWindow = new PopupWindow(view, mChooseCharWidth, mChooseCharHeight); //设置相关焦点 mCharPopupWindow.setFocusable(true); mCharPopupWindow.setOutsideTouchable(true); //设置索引popup的偏移量 int xPos = 250; //mCharPopupWindow.getWidth() * 4,350 //int resourced = mContext.getResources().getIdentifier("status_bar_height", "dimen", "android"); int yPos = 330; log("showCharPopupListView PopupWindow xPos:" + xPos + ",yPos:" + yPos); if (mCharPopupWindow.isShowing()) { mCharPopupWindow.update(); } else { //mCharPopupWindow.showAsDropDown(this, xPos, yPos); mCharPopupWindow.showAtLocation(getRootView(), Gravity.TOP, xPos, yPos); } //当索引列表显示后,延时3秒,使其自动消失 sendHandlerMessage(DISMISS_CHAR_POPUP_AFTER_THREE_SECOND); //索引列表事件的监听 charListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { //获取点击索引字符对应联系人列表的position int pos = getChooseCharPosition(position); log("showCharPopupListView onItemClick position:" + position + ",pos:" + pos); chooseCharItemClicked(pos + 1); dismissCharPopup(); } }); } /** * 自定义Adapter用于显示联系人索引列表 */ public class ChooseCharAdapter extends ArrayAdapter { Context context; int layoutResourceID; int textResourceID; ArrayList charList = new ArrayList(); public ChooseCharAdapter(Context context, int layoutResource, int textResource, ArrayList objects) { super(context, layoutResource, textResource, objects); this.context = context; this.layoutResourceID = layoutResource; this.textResourceID = textResource; this.charList = objects; }@Override public int getCount() { return charList.isEmpty() ? 0 : charList.size(); }@Override public Object getItem(int position) { return charList.get(position); }@Override public long getItemId(int position) { return position; }@Override public View getView(int position, View convertView, ViewGroup parent) { if (null == convertView) { convertView = (LinearLayout) LayoutInflater.from(context).inflate(layoutResourceID, null); } TextView textView = (TextView) convertView.findViewById(textResourceID); LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) textView.getLayoutParams(); lp.width = mChooseCharWidth; lp.height = mChooseCharWidth; textView.setLayoutParams(lp); textView.setText((CharSequence) getItem(position)); return convertView; } } //根据右侧A~Z字母获取的焦点,将联系人列表滚动到该位置 private void alphabetItemClicked(int pos) { if (mOnItemClickListener != null && pos >= 0 && pos < mAlphabet.length) { String character = mAlphabet[pos] != null ? mAlphabet[pos] : null; mOnItemClickListener.onAlphabetItemClick(character); } } //根据索引字符获取的焦点,将联系人列表滚动到该位置 private void chooseCharItemClicked(int position) { if (mOnItemClickListener != null && 0 <= position && position < chooseCharInfo.size()) { mOnItemClickListener.onCharItemClick(position); } } //定义接口,实现上面两个联系人滚动操作,具体实现将在ContactEntryListFragment.java类中,后面会讲到 public interface OnItemClickListener { void onAlphabetItemClick(String character); void onCharItemClick(int position); }private void dismissPopup() { if (mPopupWindow != null) { mPopupWindow.dismiss(); } }public void dismissCharPopup() { if (mCharPopupWindow != null) { mCharPopupWindow.dismiss(); } }public void sendHandlerMessage(int mes) { mHandler.removeMessages(mes); mHandler.sendEmptyMessageDelayed(mes,3000); }public void setChooseCharInfo(List chooseCharInfo) { this.chooseCharInfo = chooseCharInfo; }public void setOnItemClickListener(OnItemClickListener listener) { this.mOnItemClickListener = listener; } //构造字符索引列表数据 private void setChooseCharList(int pos) { mChooseCharList = new ArrayList(); if (!chooseCharInfo.isEmpty() && pos >= 0 && pos < mAlphabet.length) { for (ContentValues charinfo : chooseCharInfo) { if (null != charinfo && null != mAlphabet[pos]) { String display_name = charinfo.getAsString("display_name"); if (mAlphabet[pos].equals(getFirstChar(display_name))) { if (!display_name.isEmpty() && !mChooseCharList.contains(display_name.substring(0, 1))) { mChooseCharList.add(display_name.substring(0, 1)); } if (mChooseCharList.size() >= (MAX_CHOOSE_CHAR_LIST - 1)) { break; } } } } } } //将索引列表数据再次处理,即:第一个位置使用对应汉子字母大写代替,最后一个位置(第9位)使用...代替 private ArrayList getListinfo(ArrayList arrayList) { ArrayList list = new ArrayList(); if (arrayList.isEmpty()) { return arrayList; } String charStr = getFirstChar(arrayList.get(0)); list.add(charStr); //add char for (String str : arrayList) { list.add(str); } if (!list.isEmpty() && list.size() >= MAX_CHOOSE_CHAR_LIST) { list.set(MAX_CHOOSE_CHAR_LIST - 1, NINTH_STRING); } return list; } //根据索引列字符获取该联系在contacts列表中的position private int getChooseCharPosition(int item) { int position = -2; if (0 == item || 1 == item) { return position; } if (!mChooseCharList.isEmpty() && MAX_CHOOSE_CHAR_LIST > item && item >= 0 && !chooseCharInfo.isEmpty()) { String name = mChooseCharList.get(item - 1); for (ContentValues charinfo : chooseCharInfo) { if (null != charinfo && null != charinfo.getAsString("display_name")) { if (charinfo.getAsString("display_name").substring(0, 1).equals(name)) { position = charinfo.getAsInteger("display_position"); break; //get first position } } } } mChooseCharList.clear(); return position; }private String getFirstChar(String str) { if (str == null) { return "#"; } if (str.trim().length() == 0 || "#".equals(str)) { return "#"; } char[] c = str.toCharArray(); //deal with the name start with 0-9 if (c[0] >= '0' && c[0] <= '9') { return "#"; } String result = HanziToPinyin.getPingYinFormString(str).trim(); return result.substring(0, 1); }public String getCharString(int item) { String text = ""; if (0 == item) { text = "★"; } else if ((mAlphabet.length - 1) == item) { text = "#"; } else { text = Character.toString((char) ('A' + item - 1)); } return text; }public boolean isChar(String srcStr) { char srcChar = srcStr.charAt(0); if ((srcChar >= 'a' && srcChar <= 'z') || (srcChar >= 'A' && srcChar <= 'Z') || srcChar == '#' || srcChar == '★') { return true; } return false; }public boolean isDigit(String srcStr) { char srcChar = srcStr.charAt(0); if (srcChar >= '0' && srcChar <= '9') { return true; } return false; }private void log(String str) { if (DEBUG_FLAG) { Log.d(TAG, str); } } }

c. 在 ContactEntryListFragment.java类中实现 具体联系人滚动效果,在onLoadFinished方法load联系人时保存联系人列表数据chooseCharInfo,并通过mContactsBladeView.setChooseCharInfo(chooseCharInfo); 传递给自定义view mContactsBladeView,具体如下:
@Override public void onLoadFinished(Loader loader, Cursor data) { ..... if (chooseCharInfo.size() > 0) { chooseCharInfo.clear(); // = new ArrayList(); } if (mContactsBladeView != null) { mContactsBladeView.clearIndexer(); } if (data != null) { data.moveToPosition(-1); while (data.moveToNext()) { ContentValues chooseCharInfoCV = new ContentValues(); int position = data.getPosition(); if (position != -1) { int clos = data.getColumnIndex("display_name"); if (clos != -1) { String display_name = data.getString(clos); String account_type = data.getString(2); //Log.d("HQContactsBladeView", "ContactEntryListFragment name:" + display_name + ",position:" + position); //将联系人姓名和position放置在ContentValues中 chooseCharInfoCV.put("display_name", display_name); chooseCharInfoCV.put("display_position", position); chooseCharInfo.add(chooseCharInfoCV); } } } }if (chooseCharInfo.size() > 0) { mContactsBladeView.setChooseCharInfo(chooseCharInfo); } ..... }

在onCreateView()方法中获取并实现具体滚动效果咯。

@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { ...... //获取view布局,并设置监听 mContactsBladeView = (ContactsBladeView) getView().findViewById(R.id.blade); if (mContactsBladeView != null) { //实现ContactsBladeView.OnItemClickListener事件,并具体处理该效果 mContactsBladeView.setOnItemClickListener(new ContactsBladeView.OnItemClickListener() { @Override public void onAlphabetItemClick(String character) { setListSelection(character); }@Override public void onCharItemClick(int position) { setListSelection(position); } }); } ...... }

private void setListSelection(String character) { if (null == character) { return; } //如果是星号字符,那直接跳转至联系人收藏item位置 //Log.d("ContactsBladeView", "setListSelection character:" + character); if ("★".equals(character)) { //由于联系人列表添加有Group/User profile/Favorites三个HeaderViews,因此自动时需要去掉该部分 int position = getAdapter().getPositionForSection(0) + mListView.getHeaderViewsCount(); //"☆" mListView.setSelection(position); return; } String[] Sections = (String[]) getAdapter().getSections(); for (int i = 0; i < Sections.length; i++) { if (null != character && null != character.toCharArray() && null != Sections[i] && Sections[i].length() > 0 && Sections[i].charAt(0) == character.toCharArray()[0]) { //拿到该字符第一次出现的位置,并滚动到该位置,使其置顶显示 /*Log.d("ContactsBladeView", "setListSelection Sections[i].charAt(0):" + Sections[i].charAt(0) + ",length:" + Sections[i].length() + ",toString:" + Sections[i].toString()); */ int position = getAdapter().getPositionForSection(i) + mListView.getHeaderViewsCount(); //"☆" mListView.setSelection(position); break; } } return; }

//重写setListSelection方法,当字符索引点击后,mContactsBladeView会根据该字符查询到该联系人的position,此处直接根据该position滚动 private void setListSelection(int position) { //Log.d("HQContactsBladeView", "setListSelection position:" + position); if (0 > position || position >= chooseCharInfo.size()) { return; } mListView.smoothScrollToPosition(position); return; }

同时还有res/layout/char_item.xml,res/layout/choose_char_layout.xml两个布局文件,这就不在贴出来啦,很简单的一个TextView和一个listview布局。 至此,联系人列表右侧A~Z字母定位栏功能已经实现咯,那么再回来,我们自定义view是不是很强大呢,或者说,这样的布局,系统是没有这样的基本控件让我们来使用的,也只能通过自定义方式来实现这个feature咯。

接着回到我们的主题,讲解第三个自定义view的使用;
3、继承控件
继承控件就是继承已有的控件,创建新控件,保留继承父控件的特性,并且还可以引入新特性。下面就以支持横向滑动删除列表项的自定义ListView的实现来介绍。
a. 创建删除按钮布局delete_btn.xml,这个布局是在横向滑动列表项后显示的:


b. 创建CustomListView类,继承自ListView,并实现了OnTouchListener和OnGestureListener接口:
public class CustomListView extends ListView implements OnTouchListener, OnGestureListener {// 手势动作探测器 private GestureDetector mGestureDetector; // 删除事件监听器 public interface OnDeleteListener { void onDelete(int index); }private OnDeleteListener mOnDeleteListener; // 删除按钮 private View mDeleteBtn; // 列表项布局 private ViewGroup mItemLayout; // 选择的列表项 private int mSelectedItem; // 当前删除按钮是否显示出来了 private boolean isDeleteShown; public CustomListView(Context context, AttributeSet attrs) { super(context, attrs); // 创建手势监听器对象 mGestureDetector = new GestureDetector(getContext(), this); // 监听onTouch事件 setOnTouchListener(this); }// 设置删除监听事件 public void setOnDeleteListener(OnDeleteListener listener) { mOnDeleteListener = listener; }// 触摸监听事件 @Override public boolean onTouch(View v, MotionEvent event) { if (isDeleteShown) { hideDelete(); return false; } else { return mGestureDetector.onTouchEvent(event); } }@Override public boolean onDown(MotionEvent e) { if (!isDeleteShown) { mSelectedItem = pointToPosition((int) e.getX(), (int) e.getY()); } return false; }@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // 如果当前删除按钮没有显示出来,并且x方向滑动的速度大于y方向的滑动速度 if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) { mDeleteBtn = LayoutInflater.from(getContext()).inflate( R.layout.delete_btn, null); mDeleteBtn.setOnClickListener(new OnClickListener() {@Override public void onClick(View v) { mItemLayout.removeView(mDeleteBtn); mDeleteBtn = null; isDeleteShown = false; mOnDeleteListener.onDelete(mSelectedItem); } }); mItemLayout = (ViewGroup) getChildAt(mSelectedItem - getFirstVisiblePosition()); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); params.addRule(RelativeLayout.CENTER_VERTICAL); mItemLayout.addView(mDeleteBtn, params); isDeleteShown = true; }return false; }// 隐藏删除按钮 public void hideDelete() { mItemLayout.removeView(mDeleteBtn); mDeleteBtn = null; isDeleteShown = false; }public boolean isDeleteShown() { return isDeleteShown; }/** * 后面几个方法本例中没有用到 */ @Override public void onShowPress(MotionEvent e) {}@Override public boolean onSingleTapUp(MotionEvent e) { return false; }@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }@Override public void onLongPress(MotionEvent e) {}}

c. 定义列表项布局custom_listview_item.xml,它的结构很简单,只包含了一个TextView,这里就不在贴出来了; d. 定义适配器类CustomListViewAdapter,继承自ArrayAdapter;

public class CustomListViewAdapter extends ArrayAdapter {public CustomListViewAdapter(Context context, int textViewResourceId, List objects) { super(context, textViewResourceId, objects); }@Override public View getView(int position, View convertView, ViewGroup parent) { View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate( R.layout.custom_listview_item, null); } else { view = convertView; }TextView contentTv = (TextView) view.findViewById(R.id.content_tv); contentTv.setText(getItem(position)); return view; }}

e. 在activity_main.xml中引入自定义的ListView;

f. 在MainActivity中对列表做初始化、设置列表项删除按钮点击事件等处理;

public class MainActivity extends Activity {// 自定义Lv private CustomListView mCustomLv; // 自定义适配器 private CustomListViewAdapter mAdapter; // 内容列表 private List contentList = new ArrayList(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); initContentList(); mCustomLv = (CustomListView) findViewById(R.id.custom_lv); mCustomLv.setOnDeleteListener(new OnDeleteListener() {@Override public void onDelete(int index) { contentList.remove(index); mAdapter.notifyDataSetChanged(); } }); mAdapter = new CustomListViewAdapter(this, 0, contentList); mCustomLv.setAdapter(mAdapter); }// 初始化内容列表 private void initContentList() { for (int i = 0; i < 20; i++) { contentList.add("内容项" + i); } }@Override public void onBackPressed() { if (mCustomLv.isDeleteShown()) { mCustomLv.hideDelete(); return; } super.onBackPressed(); }}

g. 运行效果如下;
Android自定义View(一)
文章图片

哎呀,累屎了,终于看到了结尾,说实话,目前这个项目很多需求让自己学习了很多,同时给大家推介一位高手,高手ID:http://my.csdn.net/sinyu890807,在他的为文章中自己学习了很多,很多东西都可以在他的博客上了解到,简直就是高手中的高手啊。。。同时,自己也是出于学习阶段,以上有不对或者错误内容,还请给予指正,谢谢~
【Android自定义View(一)】好了,不扯了,加班来写完这篇文章啊,该下班回家坐热炕头了,,

    推荐阅读