編輯:關於Android編程
這幾天在做IM模塊,設計圖要求做一個類似下圖所示的自定義控件。
我百度了一下,發現類似的Ddmo有很多,但是還不能完全滿足設計圖的需求。 參考了幾個比較有價值的demo琢磨了一天總算做出來了,現在發出來和大家分享。 分析一下這個需求的難點。 1、右邊側滑欄(SideBar)控件繪制。 2、將列表中的中文昵稱轉化為拼音列表。(這個問題用jpinyin解決。) 3、滑動側滑欄(SideBar)的過程中如何與ListView列表建立對應的聯系? 如果能夠解決這三個問題那麼這個需求也就迎刃而解了。 接下來看看整個Demo的結構圖
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec. getMode(heightMeasureSpec) ; int finalWidth = 0, finalHeight = 0; // 測量單個字符的寬度和高度 float charWidthAndHeight = paint.measureText( letter.get(0 )) + letterSpace; if (widthMode == MeasureSpec. EXACTLY) { finalWidth = MeasureSpec.getSize (widthMeasureSpec); } else if (widthMode == MeasureSpec.AT_MOST) { finalWidth = (int ) charWidthAndHeight + getPaddingLeft() + getPaddingRight(); } if (heightMode == MeasureSpec.EXACTLY) { finalHeight = MeasureSpec.getSize (heightMeasureSpec); } else if (heightMode == MeasureSpec.AT_MOST) { // 注意measureText的值與 paint.setTextSize的值有關 finalHeight = ( int) charWidthAndHeight * letter .size() + getPaddingBottom() + getPaddingTop(); } setMeasuredDimension(finalWidth, finalHeight); }
@Override protected void onDraw(Canvas canvas) { //27 個字符(包含 #)均分整個視圖的高度,例如視圖高度為 270,270/27 均分之後,每個字符的y坐標為10。 y = getHeight() / letter.size() ; for ( int i = 0 ; i < letter .size(); i++) { Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); // 計算繪制字符 X的坐標,整個視圖寬度的一半減去字符寬度的一半 x = getWidth() / 2 - ( int) paint.measureText(letter .get(i)) / 2; // int correctY=y*i+y; // int correctY = y * i + y / 2; int correctY = ((y * i + y) + (y * i) - fontMetricsInt.bottom - fontMetricsInt.top) / 2; String tempString = isLetterUpper ? letter.get(i).toUpperCase() : letter .get(i).toLowerCase(); canvas.drawText(tempString , x, correctY, paint ); // canvas.drawLine(0, y * i + y, 100, y * i + y, paint); } }關於繪制字符X坐標的求法比較簡單,注釋已經解釋得很清楚就不再分析。 主要分析y坐標的求法。我百度了一些demo總結發現有兩種關於字符y坐標的求法。 (為了便於觀察,我暫時將SideBar的背景改為了淺藍色,並且每個字符的都用橫線分隔) 第一種是int correctY=y*i+y。運行一下程序發現字符的繪制明顯向下偏移(如下圖所示)
第二種是int correctY=y*i+y/2。再運行程序,發現還是有點偏移。我是處女座有強迫症,果斷不能忍啊。
於是在網上搜索了不少資料,研究了一番。 我把計算公式改為int correctY = ((y * i + y) + (y * i) - fontMetricsInt.bottom - fontMetricsInt.top) / 2; 運行程序,完美解決問題。
剛看到這個式子可能不是很理解。我給大家解釋一下,假設我們要繪制D這個字符。如圖所示:
這時包含D字符的這個矩形上下邊的縱坐標分別是y*i,y*i+y。那麼這個矩形的中間縱坐標怎麼算呢?答案就是((y*i)+(y*i+y))/2 得到這個中間值之後,我們直接調用繪制方法canvas.drawText(tempString, x, ((y*i)+(y*i+y))/2 , paint);發現還是出問題了 字符還是畫在矩形偏上位置(如下圖)
/** * 回調接口。 */ public interface OnCurrentLetterListener { void showCurrentLetter(String currentLetter); void hideCurrentLetter (); }
@Override public boolean onTouchEvent(MotionEvent event) { float y = event.getY(); // 獲取當前側滑欄字母的下標 float currentLetterIndex = y / getHeight() * letter.size(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: if (onCurrentLetterListener != null) { //對上下邊界對限制 if (currentLetterIndex >= letter.size()) { currentLetterIndex = letter.size() - 1 ; } else if (currentLetterIndex < 0) { currentLetterIndex = 0; } onCurrentLetterListener .showCurrentLetter(letter.get(( int) currentLetterIndex)); } return true; case MotionEvent. ACTION_UP: if (onCurrentLetterListener != null) { onCurrentLetterListener.hideCurrentLetter() ; } return true; default: return true; } }最後整個方法return true;表示這個觸摸事件被SideBar視圖消費了,不再向其它地方傳遞事件。 2、最後貼個SideBar類的代碼
package per.edward.ui; import android.content.Context ; import android.content.res.TypedArray ; import android.graphics.Canvas ; import android.graphics.Color ; import android.graphics.Paint ; import android.util.AttributeSet ; import android.view.MotionEvent ; import android.view.View ; import java.util.ArrayList ; import java.util.Arrays ; import java.util.List ; /** * 側滑欄視圖 * Created by Edward on 2016/4/27. */ public class SideBar extends View { private String[] letterStrings = { "#", "A" , "B", "C", "D" , "E", "F" , "G", "H", "I" , "J", "K", "L" , "M", "N", "O" , "P", "Q", "R", "S" , "T", "U", "V" , "W", "S", "Y" , "Z"} ; private Paint paint; // 字母列表 private Listletter; // 繪制字母的 x,y坐標 private int x, y ; // 字母的間距 private int letterSpace = 0 ; // 字母是否大寫 private boolean isLetterUpper = true; private OnCurrentLetterListener onCurrentLetterListener; public void setLetter (List letter) { this .letter = letter ; } // 設置回調接口 public void setOnCurrentLetterListener(OnCurrentLetterListener onCurrentLetterListener) { this .onCurrentLetterListener = onCurrentLetterListener ; } public SideBar(Context context) { this (context, null) ; } public SideBar(Context context, AttributeSet attrs) { super (context, attrs) ; if ( letter == null ) { letter = new ArrayList<>() ; letter = Arrays.asList(letterStrings ); } TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SideBar) ; letterSpace = typedArray.getInt(R.styleable.SideBar_SB_Letter_Space, 0); isLetterUpper = typedArray.getBoolean(R.styleable.SideBar_SB_Is_Letter_Upper, true); typedArray.recycle() ; paint = new Paint() ; paint .setColor(Color.BLACK) ; paint .setAntiAlias(true) ; paint .setTextSize(30) ;//3CAC48 paint .setColor(Color.parseColor("#ffffff" )); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec. getMode(heightMeasureSpec) ; int finalWidth = 0, finalHeight = 0; // 測量單個字符的寬度和高度 float charWidthAndHeight = paint.measureText( letter.get(0 )) + letterSpace; if (widthMode == MeasureSpec. EXACTLY) { finalWidth = MeasureSpec.getSize(widthMeasureSpec); } else if (widthMode == MeasureSpec.AT_MOST) { finalWidth = (int) charWidthAndHeight + getPaddingLeft() + getPaddingRight(); } if (heightMode == MeasureSpec.EXACTLY) { finalHeight = MeasureSpec.getSize(heightMeasureSpec) ; } else if (heightMode == MeasureSpec.AT_MOST) { // 注意measureText的值與 paint.setTextSize的值有關 finalHeight = ( int) charWidthAndHeight * letter .size() + getPaddingBottom() + getPaddingTop(); } // Log.e("--------------->", MeasureSpec.getSize(widthMeasureSpec) + " " + MeasureSpec.getSize(heightMeasureSpec)); setMeasuredDimension(finalWidth , finalHeight); } @Override protected void onDraw(Canvas canvas) { setBackgroundColor(Color.parseColor ("#31b2f7" )); //27 個字符(包含 #)均分整個視圖的高度,例如視圖高度為 270,270/27 均分之後,每個字符的 y坐標為10 y = getHeight() / letter.size() ; Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); for ( int i = 0 ; i < letter .size(); i++) { // 計算繪制字符 X的坐標,整個視圖寬度的一半減去字符寬度的一半 x = getWidth() / 2 - ( int) paint.measureText(letter .get(i)) / 2; // int correctY=y*i+y; // int correctY = y * i + y / 2; int correctY = ((y * i + y) + (y * i) - fontMetricsInt.bottom - fontMetricsInt.top) / 2; String tempString = isLetterUpper ? letter.get(i).toUpperCase() : letter .get(i).toLowerCase(); canvas.drawText(tempString , x, ((y * i) + ( y * i + y)) / 2 , paint) ; canvas.drawLine( 0, y * i + y, 100, y * i + y, paint); } } @Override public boolean onTouchEvent(MotionEvent event) { float y = event.getY(); // 獲取當前側滑欄字母的下標 float currentLetterIndex = y / getHeight() * letter.size(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: if ( onCurrentLetterListener != null ) { //對上下邊界對限制 if (currentLetterIndex >= letter.size()) { currentLetterIndex = letter.size() - 1 ; } else if (currentLetterIndex < 0) { currentLetterIndex = 0; } onCurrentLetterListener .showCurrentLetter(letter.get(( int) currentLetterIndex)); } return true; case MotionEvent. ACTION_UP: if ( onCurrentLetterListener != null ) { onCurrentLetterListener.hideCurrentLetter() ; } return true; default: return true; } } /** * 回調接口。 */ public interface OnCurrentLetterListener { void showCurrentLetter(String currentLetter); void hideCurrentLetter (); } }
package per.edward.ui; import android.app.Activity ; import android.os.Bundle ; import android.view.View ; import android.widget.TextView ; /** * author:Edward */ public class MainActivity extends Activity { private TextView txtShowCurrentLetter; private SideBar sideBar; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState) ; setContentView(R.layout. activity_main); txtShowCurrentLetter = (TextView) findViewById(R.id.txt_show_current_letter ); sideBar = (SideBar) findViewById(R.id. side_bar); setCallbackInterface() ; } /** * 設置回調接口 */ public void setCallbackInterface() { // 回調接口 sideBar .setOnCurrentLetterListener(new SideBar.OnCurrentLetterListener() { @Override public void showCurrentLetter(String currentLetter) { txtShowCurrentLetter .setVisibility(View.VISIBLE) ; txtShowCurrentLetter.setText(currentLetter) ; } @Override public void hideCurrentLetter() { txtShowCurrentLetter.setVisibility(View. GONE); } }); } }
分享到這裡整個demo的流程已經走到一半了。 下面會講講ListView與SideBar控件的聯動處理。 二、建立側滑欄SideBar與ListView的對應關系 1、首先創建一個聯系人的實體類,在這個實體類中,定義了兩個變量firstLetter(用來存儲聯系人拼音的第一個字母)和name(聯系人的名字)。
package per.edward.ui; /** * 聯系人列表實體類 * Created by Edward on 2016/4/26. */ public class ContactsModel { private String firstLetter; private String name; public String getFirstLetter() { return firstLetter; } public void setFirstLetter(String firstLetter) { this .firstLetter = firstLetter ; } public String getName() { return name; } public void setName(String name) { this .name = name ; } }
SideBarAdapter接收到此布局之後,會通過convertView = LayoutInflater.from(context).inflate(mItemLayoutId, null);加載布局。 並且在getView方法中動態在控制字母標題的顯示或隱藏。 3、接著創建一個名為SideBarAdapter類的適配器並且繼承BaseAdapter。 (1)構造方法中調用traverseList方法。這個方法的算法是這樣的。此時mDatas列表所有的getFirstLetter字段已經排序成 ###ABCDEFLLLLMMMQTTZZ。#有三個,for循環。 第一次currentd的值是#把current的值作為鍵並且記錄它的下標,即map.put(current, i)。 第二次current的值是#不記錄,第三次current的值還是# 不記錄。 到第四次此時current更新為A,把current作為鍵並且記錄下標。 第五次此時current更新為B,把current作為鍵並且記錄下標....一直到for循環結束為止。 這時map的集合已經記錄了整個mDatas列表的字母標題和下標。我們只需要在getView方法(這個方法會傳進來一個position)中判斷當前的position是否等於map的下標值。如果相等就將字母標題顯示出來。否則隱藏
package per.edward.ui; import android.content.Context ; import android.view.LayoutInflater ; import android.view.View ; import android.view.ViewGroup ; import android.widget.BaseAdapter ; import android.widget.TextView ; import java.util.HashMap ; import java.util.List ; import java.util.Map ; /** * 側欄適配器 * Created by Edward on 2016/4/27. */ public class SideBarAdapter extends BaseAdapter { // 是否第一個 Item的 private boolean isFirstItemLetter = true; // 記錄是否顯示字母標題,鍵為字母,值為下標 private Mapmap; private List mDatas; private int mItemLayoutId ; private Context context; public SideBarAdapter(Context mContext , List mDatas, int mItemLayoutId) { this .mDatas = mDatas ; map = new HashMap<>() ; this. mItemLayoutId = mItemLayoutId; this. context = mContext; traverseList() ; } /** * 遍歷列表 * 由於傳進來的 mDatas是一個已排好序的列表,遍歷整個列表,每遇到分類的第一個字母就把下標記錄下來 */ public void traverseList() { // 獲取初始值 String current = mDatas.get(0 ).getFirstLetter(); for ( int i = 0 ; i < mDatas .size(); i++) { char tempChar = mDatas.get(i).getFirstLetter().charAt(0) ; String tempFirstLetter = mDatas.get(i).getFirstLetter(); if (tempFirstLetter.equals(current) || (tempChar < 'A' || tempChar > 'Z' )) { if ( isFirstItemLetter) { map.put(current , i); } } else { //更新初始值 current = mDatas .get(i).getFirstLetter(); map.put(current , i); } isFirstItemLetter = false; } } /** * 獲取當前字母的下標 * * @return */ public int getCurrentLetterPosition(String currentLetter) { if (map.get(currentLetter) != null) { return map.get(currentLetter); } else return - 1; } @Override public int getCount() { return mDatas.size(); } @Override public Object getItem( int position) { return mDatas.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView( int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if (convertView == null) { viewHolder = new ViewHolder() ; convertView = LayoutInflater.from( context).inflate(mItemLayoutId , null); viewHolder. txtFirstLetter = (TextView) convertView.findViewById(R.id.txt_letter_category) ; viewHolder. txtName = (TextView) convertView.findViewById(R.id.txt_name) ; convertView.setTag(viewHolder) ; } else { viewHolder = (ViewHolder) convertView.getTag(); } // 判斷是否顯示字母標題 if (map.get( mDatas.get(position).getFirstLetter()) != null && map.get(mDatas .get(position).getFirstLetter()).equals(position)) { viewHolder.txtFirstLetter .setVisibility(View.VISIBLE) ; viewHolder.txtFirstLetter.setText( mDatas.get(position).getFirstLetter()); } else { viewHolder.txtFirstLetter .setVisibility(View.GONE) ; } viewHolder.txtName .setText(mDatas.get(position).getName()) ; return convertView ; } public class ViewHolder { TextView txtFirstLetter , txtName; } }
package per.edward.ui; import android.os.Bundle ; import android.support.v7.app.AppCompatActivity ; import android.view.View ; import android.widget.ListView ; import android.widget.TextView ; import java.util.ArrayList ; import java.util.Collections ; import java.util.Comparator ; import java.util.HashSet ; import java.util.List ; import java.util.Set ; import opensource.jpinyin.PinyinHelper ; /** * author:Edward * 此 demo的博客地址:http://blog.csdn.net/u012814441 */ public class MainActivity extends AppCompatActivity { private ListView listView; private TextView txtShowCurrentLetter; private SideBar sideBar; private SideBarAdapter myAdapter; // private Listlist; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState) ; setContentView(R.layout. activity_main); txtShowCurrentLetter = (TextView) findViewById(R.id.txt_show_current_letter ); sideBar = (SideBar) findViewById(R.id. side_bar); listView = (ListView) findViewById(R.id. list_view); setCallbackInterface() ; List list = initData() ; chineseToPinyin(list) ; // 將聯系人列表的標題字母排序 Collections. sort(list , new Comparator () { @Override public int compare(ContactsModel lhs, ContactsModel rhs) { return lhs.getFirstLetter().compareTo(rhs.getFirstLetter()) ; } }); // 將聯系人列表的標題字母放到 List 列表中,准備數據去重 List getLetter = new ArrayList<>(); for ( int i = 0 ; i < list.size(); i++) { getLetter.add(list.get(i).getFirstLetter()); } // 數據去重 getLetter = removeDuplicate(getLetter) ; // 將聯系人列表的字母標題排序 Collections. sort(getLetter , new Comparator () { @Override public int compare(String lhs, String rhs) { return lhs.compareTo(rhs) ; } }); // 設置已排序好的標題 sideBar .setLetter(getLetter); myAdapter = new SideBarAdapter( this, list, R.layout.adapter_side_bar) ; listView .setAdapter(myAdapter) ; } /** * 將中文轉化為拼音 */ public void chineseToPinyin(List list) { for (int i = 0; i < list.size() ; i++) { ContactsModel contactsModel1 = list.get(i); // 將漢字轉換為拼音 String pinyinString = PinyinHelper.getShortPinyin(list.get(i).getName()) ; // 將拼音字符串轉換為大寫拼音 String upperCasePinyinString = String.valueOf(pinyinString.charAt( 0)).toUpperCase(); // 獲取大寫拼音字符串的第一個字符 char tempChar = upperCasePinyinString.charAt( 0); if (tempChar < 'A' || tempChar > 'Z' ) { contactsModel1.setFirstLetter( "#"); } else { contactsModel1.setFirstLetter(String.valueOf(tempChar)) ; } } } /** * 設置回調接口 */ public void setCallbackInterface() { // 回調接口 sideBar .setOnCurrentLetterListener(new SideBar.OnCurrentLetterListener() { @Override public void showCurrentLetter(String currentLetter) { txtShowCurrentLetter .setVisibility(View.VISIBLE) ; txtShowCurrentLetter.setText(currentLetter) ; int position = myAdapter.getCurrentItemPosition(currentLetter); if (position != -1 ) listView.setSelection(position) ; } @Override public void hideCurrentLetter() { txtShowCurrentLetter.setVisibility(View. GONE); } }); } /** * 初始化數據 */ public List initData() { List list = new ArrayList<>(); ContactsModel contactsModel ; String[] nameStrings = { "覃" , "岑 ", "$ 來啊,來互相傷害啊 ", "疍姬" , "梵蒂岡 ", " 亳州", "佟" , "郄 ", " 張三", "Edward", " 李四", "萌萌哒" , "霾耷 ", " 離散", "趙信" , "啦啦 ", " 辣妹子", "嗷嗷" , "妹妹 ", "']asd" , "%Hello"} ; for ( int i = 0 ; i < nameStrings.length ; i++) { contactsModel = new ContactsModel() ; contactsModel.setName(nameStrings[i]) ; list.add(contactsModel) ; } return list; } /** * 去重數據 * * @param list * @param * @return */ public List< T> removeDuplicate (List list) { Set h = new HashSet<>(list) ; list.clear() ; list.addAll(h) ; return list ; } }
7、最終效果圖
自定義輪播圖CarouselView 自定義下拉刷新PullRefreshListView 馬上就要正式做畢業設計了,一些零碎時間寫其中的一個模塊,現記錄下來,以備以後忘
我們已經有文章向你描述如何使用<include />標簽來重用和共享你的布局代碼。這篇文章將向你闡述<merge />標簽的使用以及如何與<
我們在用手機的時候,如果來了短信,而我們沒有點擊查看的話,是不是在手機的最上邊的狀態欄裡有一個短信的小圖標提示啊?你是不是也想實現這種功能呢?今天的Notificatio
隨著Android手機的屏幕越來越大,Android浮動窗口的應用也越來越多。像經常會用到的,音樂播放器的桌面歌詞以及一些手機衛士軟件,像騰訊手機管家的小火箭清理內存,都