Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義字母索引View

自定義字母索引View

編輯:關於Android編程

在聯系人,好友等列表中,為了能夠快速的根據名稱查找到相應的聯系人或者好友,通常會建立一個可以根據字母快速定位名稱的View。如下圖中右邊字母表所示:
聯系人列表的字母索引View

1.自定義View

關於自定義View需要注意的事項:

讓View支持padding 讓View支持wrap_content 如果是View,只需重寫onDraw()方法;如果是ViewGroup,有必要重寫onMeasure()和onLayout()方法。

在自定義字母索引View中,為了與普通的View有相同的屬性,當然需要支持padding;對於支持wrap_content,為了防止在這種自定義View情況下,使用wrap_content就相當於match_parent的情形,可以在onMeasure()中定義一個最小的默認的寬高度,以避免View顯示不正常;對於字母索引View需要重寫的方法,它是一個View,所以只需重寫onDraw()方法。

2.繪制字母

繪制字母時應該知道相關的信息:

字母的起始xy坐標 字母的大小

繪制字母的要求:

居中顯示 可以改變字母的字體顏色 可以改變字母的字體大小

下面為onDraw()方法中,繪制字母表的代碼:

        int paddingLeft = getPaddingLeft();
        //view實際寬度
        int width = getWidth() - paddingLeft - getPaddingRight();
        //繪制字符時的起始x坐標
        float startX = paddingLeft + (float) width / 2;

        int paddingTop = getPaddingTop();
        //view實際高
        int height = getHeight() - paddingTop - getPaddingBottom();
        int length = mCharacters.length();
        //每個字符所擁有的高度
        float characterHeight = (float) height / length;
        //繪制字符時的起始y坐標
        float startY = paddingTop + characterHeight / 2;
        //繪制的字符的界限
        Rect bounds = new Rect();
        mCharacterYValues = new int[length + 1];
        for (int i = 0; i < length; i++) {
            mPaint.getTextBounds(mCharacters, i, i + 1, bounds);
            float x = startX - (float) (bounds.left + bounds.right) / 2;
            float y = startY + i * characterHeight - (float) (bounds.top + bounds.bottom) / 2;
            canvas.drawText(mCharacters, i, i + 1, x, y, mPaint);
            //記錄每個字符起始的y坐標值
            mCharacterYValues[i] = (int) (paddingTop + i * characterHeight);
        }
        //最後一個字符結束的y坐標值
        mCharacterYValues[length] = (int) (paddingTop + length * characterHeight);

繪制字母的步驟:
1.求view可繪制的實際寬高度(去除padding)

view實際寬度width = getWidth() - getPaddingLeft()- getPaddingRight(); view實際高度height = getHeight() - getPaddingTop()- getPaddingBottom();

2.求字母的寬高度

繪制的字符的界限 Rect bounds = new Rect();
mPaint.getTextBounds(mCharacters, i, i + 1, bounds); 字體寬度bounds.left + bounds.right; 字體高度bounds.top + bounds.bottom

3.繪制字母

繪制字符時的起始x坐標startX = paddingLeft + (float) width / 2; 繪制字符時的起始y坐標startY = paddingTop + characterHeight / 2;(每個字符所擁有的高度characterHeight = (float) height/mCharacters.length()) 繪制字符
float x = startX - (float) (bounds.left + bounds.right) / 2;
float y = startY + i * characterHeight - (float) (bounds.top + bounds.bottom) / 2;
canvas.drawText(mCharacters, i, i + 1, x, y, mPaint);

效果圖:
字母索引View

4.處理View觸摸事件

1.定義觸摸事件監聽接口

    /**
     * 觸摸CharacterView事件的監聽接口
     */
    public interface OnCharacterTouchListener {

        /**
         * 點擊字符回調的方法
         *
         * @param view CharacterView
         * @param c    點擊的字符
         */
        void onDown(View view, char c);

        /**
         * 在字符表上移動回調的方法
         *
         * @param view CharacterView
         * @param c    當前手指觸摸的字符
         */
        void onMove(View view, char c);

    }

2.對View的觸摸事件的處理

    /**
     * 設置CharacterView的觸摸監聽器
     *
     * @param listener CharacterView觸摸監聽器
     */
    public void setOnCharacterTouchListener(final OnCharacterTouchListener listener) {


        if (listener == null) {
            setOnTouchListener(null);
            return;
        }

        setOnTouchListener(new View.OnTouchListener() {
            char lastCharacter = ' ';

            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int yDown = (int) motionEvent.getY();
                if (yDown > mCharacterYValues[0] && yDown < mCharacterYValues[mCharacterYValues.length - 1]) {
                    char c = findCharacter(yDown);
                    switch (motionEvent.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            listener.onDown(view, c);
                            break;
                        case MotionEvent.ACTION_MOVE:
                            if (lastCharacter == ' ' || lastCharacter != c) {
                                listener.onMove(view, c);
                                lastCharacter = c;
                            }
                            break;
                        case MotionEvent.ACTION_UP:
                            break;
                    }
                }
                return true;
            }
        });
    }

    /**
     * 根據坐標值找到對應的字符
     *
     * @param yValue y坐標值
     * @return 相應的字符
     */
    private char findCharacter(int yValue) {
        int low = 0, high = mCharacterYValues.length - 1;
        int lastMid = 0;
        while (low <= high) {
            int mid = (low + high) / 2;
            lastMid = mid;
            if (mCharacterYValues[mid] == yValue) {
                return mCharacters.charAt(mid);
            } else if (mCharacterYValues[mid] < yValue) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        int closestPointIndex = lastMid;
        //如果最後一次比較是大於目標值,則需要選擇前一個下標
        if (mCharacterYValues[lastMid] > yValue) {
            closestPointIndex -= 1;
        }
        return mCharacters.charAt(closestPointIndex);
    }

主體思想:在繪制字符表的時候,記錄每一個字符的起始y坐標值,然後在觸摸事件中,把得到的y坐標值,與記錄的所有記錄的y坐標值進行比較,得到與觸摸事件中的y坐標值最相近的一個y坐標值,就可找到當前觸摸的字符。
例如: . A . B . C . D . E . . . Z .
在繪制的時候記錄從A前面的一個y坐標值到Z最後的一個y坐標值,在查找的時候采用二分查找算法,找到觸摸的y坐標值落在的區間,這個區間所代表的就是相應的字符。

//記錄每個字符起始的y坐標值
mCharacterYValues[i] = (int) (paddingTop + i * characterHeight);
//最後一個字符結束的y坐標值
mCharacterYValues[length] = (int) (paddingTop + length * characterHeight);

5.設置默認的寬度

重寫onMeasure()方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //設置默認的寬度
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST) {
            final int defaultWidth = 80;
            setMeasuredDimension(defaultWidth, heightSpecSize);
        }
    }

不設置默認的高度的原因是:當View的layout_height=”wrap_content”時,可以讓它匹配父View的高度。

6.具體實例

布局文件activity_main.xml:


<framelayout android:id="@+id/root_layout" android:layout_height="match_parent" android:layout_width="match_parent" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" tools:context="com.wj.study.MainActivity" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">

    
</framelayout>

MainActivity代碼:

package com.wj.study;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.wj.study.view.CharacterView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CharacterView characterView = (CharacterView) findViewById(R.id.character_view);
        characterView.insertFirst('#');
        characterView.setBackgroundColor(Color.CYAN);
        characterView.setColor(Color.BLUE);
        characterView.setTextSize(getResources().getDimensionPixelSize(R.dimen.textSize_14sp));
        characterView.setOnCharacterTouchListener(new CharacterView.OnCharacterTouchListener() {
            @Override
            public void onDown(View view, char c) {
                Log.d("TAG", "onDown=" + c);
            }

            @Override
            public void onMove(View view, char c) {
                Log.d("TAG", "onMove=" + c);
                Toast.makeText(view.getContext(), String.valueOf(c), Toast.LENGTH_SHORT).show();
            }
        });

    }
}

CharacterView代碼:

package com.wj.study.view;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * Author:王江 on 2016/6/30 17:45
 * Description: CharacterView主要是根據名稱首字母的做快速查找,通常應用於聯系人列表,好友列表等。
 */
public class CharacterView extends View {
    private String mCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    private int[] mCharacterYValues = null;

    public CharacterView(Context context) {
        super(context);
    }

    public CharacterView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @TargetApi(21)
    public CharacterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public CharacterView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //設置默認的寬度
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST) {
            final int defaultWidth = 80;
            setMeasuredDimension(defaultWidth, heightSpecSize);
        }
    }

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(2.0f);

        int paddingLeft = getPaddingLeft();
        //view實際寬度
        int width = getWidth() - paddingLeft - getPaddingRight();
        //繪制字符時的起始x坐標
        float startX = paddingLeft + (float) width / 2;

        int paddingTop = getPaddingTop();
        //view實際高
        int height = getHeight() - paddingTop - getPaddingBottom();
        int length = mCharacters.length();
        //每個字符所擁有的高度
        float characterHeight = (float) height / length;
        //繪制字符時的起始y坐標
        float startY = paddingTop + characterHeight / 2;
        //繪制的字符的界限
        Rect bounds = new Rect();
        mCharacterYValues = new int[length + 1];
        for (int i = 0; i < length; i++) {
            mPaint.getTextBounds(mCharacters, i, i + 1, bounds);
            float x = startX - (float) (bounds.left + bounds.right) / 2;
            float y = startY + i * characterHeight - (float) (bounds.top + bounds.bottom) / 2;
            canvas.drawText(mCharacters, i, i + 1, x, y, mPaint);
            //記錄每個字符起始的y坐標值
            mCharacterYValues[i] = (int) (paddingTop + i * characterHeight);
        }
        //最後一個字符結束的y坐標值
        mCharacterYValues[length] = (int) (paddingTop + length * characterHeight);
    }

    /**
     * 設置字符的字體大小
     *
     * @param textSize 字體大小
     */
    public void setTextSize(float textSize) {
        mPaint.setTextSize(textSize);
    }

    /**
     * 設置畫筆的顏色
     *
     * @param color 畫筆顏色
     */
    public void setColor(int color) {
        mPaint.setColor(color);
    }

    /**
     * 插入新一個字符,與原來的字符表組成一個新的字符表
     *
     * @param c 字符
     */
    public void insertFirst(char c) {
        StringBuilder sb = new StringBuilder();
        sb.append(c);
        sb.append(mCharacters);
        mCharacters = null;
        mCharacters = sb.toString();
    }

    /**
     * 插入新一個字符,與原來的字符表組成一個新的字符表
     *
     * @param c 字符
     */
    public void insertLast(char c) {
        StringBuilder sb = new StringBuilder();
        sb.append(mCharacters);
        sb.append(c);
        mCharacters = null;
        mCharacters = sb.toString();
    }

    /**
     * 自定義字符表
     *
     * @param characters 字符串
     */
    public void setCharacter(String characters) {
        if (characters == null) return;
        mCharacters = null;
        mCharacters = characters;
    }

    /**
     * 設置CharacterView的觸摸監聽器
     *
     * @param listener CharacterView觸摸監聽器
     */
    public void setOnCharacterTouchListener(final OnCharacterTouchListener listener) {


        if (listener == null) {
            setOnTouchListener(null);
            return;
        }

        setOnTouchListener(new View.OnTouchListener() {
            char lastCharacter = ' ';

            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int yDown = (int) motionEvent.getY();
                if (yDown > mCharacterYValues[0] && yDown < mCharacterYValues[mCharacterYValues.length - 1]) {
                    char c = findCharacter(yDown);
                    switch (motionEvent.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            listener.onDown(view, c);
                            break;
                        case MotionEvent.ACTION_MOVE:
                            if (lastCharacter == ' ' || lastCharacter != c) {
                                listener.onMove(view, c);
                                lastCharacter = c;
                            }
                            break;
                        case MotionEvent.ACTION_UP:
                            break;
                    }
                }
                return true;
            }
        });
    }

    /**
     * 根據坐標值找到對應的字符
     *
     * @param yValue y坐標值
     * @return 相應的字符
     */
    private char findCharacter(int yValue) {
        int low = 0, high = mCharacterYValues.length - 1;
        int lastMid = 0;
        while (low <= high) {
            int mid = (low + high) / 2;
            lastMid = mid;
            if (mCharacterYValues[mid] == yValue) {
                return mCharacters.charAt(mid);
            } else if (mCharacterYValues[mid] < yValue) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        int closestPointIndex = lastMid;
        //如果最後一次比較是大於目標值,則需要選擇前一個下標
        if (mCharacterYValues[lastMid] > yValue) {
            closestPointIndex -= 1;
        }
        return mCharacters.charAt(closestPointIndex);
    }

    /**
     * 觸摸CharacterView事件的監聽器
     */
    public interface OnCharacterTouchListener {

        /**
         * 點擊字符回調的方法
         *
         * @param view CharacterView
         * @param c    點擊的字符
         */
        void onDown(View view, char c);

        /**
         * 在字符表上移動回調的方法
         *
         * @param view CharacterView
         * @param c    當前手指觸摸的字符
         */
        void onMove(View view, char c);

    }
}

效果圖:
聯系人列表的字母索引View

附加:對於該View如何與ListView(or RecyclerView)進行匹配,將在下一章中分析。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved