Android自定义控件

    在Android开发中,大家经常会提到自定义控件的问题,对于好多初学者来说,可以说谈之色变,其实自定义控件并没有那么难,下面我就带大家通过写一个自定义控件—–通讯录右侧的导航字母,来解释一下自定义控件的使用。

    在解释之前先给大家看一下运行的具体效果,由于我不会截取动态图,所以就普通图片给大家看一下啦,我们要实现的就是如下图中右侧的字母导航,我们可以点击右侧的某个字母来直接快速查找首字母为该字母的内容,如图:

这里写图片描述

    首先我们自定义类继承View类,这里需要注意,我们需要实现其三个构造方法;如下:

这里写图片描述

    这三个构造方法的用途可以参考注释的内容,大家可以在自己练习的时候,把其中的一个或者两个删掉,然后运行看看会报什么错误,然后根据错误提示,就可以找到该构造函数的用途。

    接下来就是在该自定义控件中绘制自己想要的内容;在这篇博客中,我们要绘制的是右侧的导航字母,其实难度并不大,代码如下:

public class ListViewSideBar extends View{

    /**
     * 右侧导航栏显示的字母的数组
     */
    private String[] mIndexers ;
    /**
     * 声明画笔
     */
    private Paint mTextPaint ;
    /**
     * 文字的颜色的字符串
     */
    private String mTextColorString ;
    /**
     * 文字的尺寸
     */
    private float mTextSize ;
    /**
     * 文字的高度,根据现有高度与需要显示的文字的数量计算得到
     */
    private float mTextHeight ;
    /**
     * 文字的尺寸相关数据封装对象,用于计算绘制文字时垂直方向的偏移量
     */
    private Paint.FontMetrics mFontMetrics ;
    /**
     * 控件的宽度
     */
    private int mViewWidth ;
    /**
     * 控件的高度
     */
    private int mViewHeight ;

    /**
     * 构造方法,将在JAVA程序中创建对象时被调用
     * @param context
     */
    public ListViewSideBar(Context context) {
        this(context, null);
    }

    /**
     * 构造方法,将在使用res\layout设计布局时被调用
     * @param context
     * @param attrs
     */
    public ListViewSideBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化
        init() ;
    }

    /**
     * 构造方法,将在使用res\layout设计布局时被调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public ListViewSideBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 初始化
     */
    private void init() {
        // 加载需要被显示的文字
        mIndexers = getResources().getStringArray(R.array.sort_key_array) ;
        // 初始化文字大小
        mTextSize = 16f ;
        // 初始化文字颜色
        mTextColorString = "#666666" ;

        // 初始化画笔
        mTextPaint = new Paint() ;
        mTextPaint.setAntiAlias(true) ;
        mTextPaint.setTextAlign(Paint.Align.CENTER) ;
        mTextPaint.setTextSize(mTextSize) ;
        mTextPaint.setColor(Color.parseColor(mTextColorString)) ;

        // 根据画笔,得到文字尺寸相关的数据
        mFontMetrics = mTextPaint.getFontMetrics() ;
    }

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

        // 获取当前控件的尺寸
        mViewWidth = getMeasuredWidth() ;
        mViewHeight = getMeasuredHeight() ;
        // 计算每个文字占据的高度
        mTextHeight = mViewHeight / mIndexers.length ;

    }

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

        // 绘制文字时水平方向的偏移量,由于画笔已经设置水平居中,则该偏移量为控件宽度的一半
        float x = mViewWidth / 2 ;
        // 绘制文字垂直方向的偏移量
        float y ;
        for (int i = 0; i < mIndexers.length; i++) {
            // 计算当前绘制的文字在垂直方向的偏移量,注:以下代码并没有实现文字在可用空间内垂直居中
            y = i * mTextHeight + mFontMetrics.ascent * -1 ;
            // 执行绘制
            canvas.drawText(mIndexers[i], x, y, mTextPaint) ;

        }

    }

}

    这是关于绘制的内容,在重写onDraw方法时,大家一定要注意的是,不要轻易的删掉父类的onDraw方法的引用super.onDraw(canvas),除非你能保证你不需要用到父类的方法,否则会报错。

    然后我们会想,一个自定义的控件功能也不至于这么单调吧,我们在通讯录中可以通过点击右侧的字母来快速查找首字母为被点击的字母的姓名,接下来我们也用这个功能来实现一下:

    /**
     * 当某个Indexer被点击时的监听器对象
     */
    private OnIndexerClickListener mOnIndexerClickListener ;

    /**
     * 设置当某个Indexer被点击时的监听器对象
     *
     * @param listener
     *            监听器对象
     */
    public void setOnIndexerClickListener(OnIndexerClickListener listener) {
        this.mOnIndexerClickListener = listener ;
    }

    /**
     * 当点击某个字母时的监听器
     */
    public static interface OnIndexerClickListener {
        /**
         *
         * 当字母导航中的文字被按下时,该方法被自动回调
         *
         * @param position 被按下的文字在列表中的索引
         * @param str 被按下的文字
         */
        void onIndexerClick(int position, String str) ;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 判断触屏操作的类型是否是按下
        if(event.getAction() == MotionEvent.ACTION_DOWN) {
            // 获取当前按下的位置
            float y = event.getY() ;
            // 计算当前按下时对应的文字的下标
            int index = (int) (y / mTextHeight) ;
            // 获取当前按下时的文字
            String str = mIndexers[index] ;
            // 回调监听器方法
            if(mOnIndexerClickListener != null) {
                mOnIndexerClickListener.onIndexerClick(index, str) ;
                return true ;
            }
        }
        return super.onTouchEvent(event);
    }

    接下来就是在需要显示该控件的界面上来调用了:

public class MainActivity extends AppCompatActivity implements ListViewSideBar.OnIndexerClickListener {

    private List<Student> students ;
    private MyAdapter adapter ;
    private ListView lvShow ;
    private ListViewSideBar sideBar ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        lvShow = (ListView) findViewById(R.id.lv_listview) ;
        students = new ArrayList<Student>() ;
        adapter = new MyAdapter(students, this) ;
        lvShow.setAdapter(adapter) ;

        sideBar = (ListViewSideBar) findViewById(R.id.lvsb_sort_keys) ;
        sideBar.setOnIndexerClickListener(this) ;

        new LoadDataTask().execute() ;

    }

    @Override
    public void onIndexerClick(int position, String str) {
        int moveToPosition = adapter.getPositionForSection(str.charAt(0)) ;
        lvShow.setSelectionFromTop(moveToPosition, 0) ;
    }

    private class LoadDataTask extends AsyncTask<Object, Object, Object> {

        @Override
        protected Object doInBackground(Object... params) {
            //获取数据
            List<Student> data = StudentDaoFactory.newInstance().getStudentList() ;
            // 排序
            Collections.sort(data, new Comparator<Student>() {
                @Override
                public int compare(Student lhs, Student rhs) {
                    return lhs.getSortKey().compareTo(rhs.getSortKey()) ;
                }
            });
            return data ;
        }

        @Override
        protected void onPostExecute(Object result) {
            List<Student> data = (List<Student>) result ;
            students.addAll(data) ;
            adapter.notifyDataSetChanged() ;
        }
    }

}

    以下是Adapter适配器的代码(关于adapter我在这里不做详细介绍了,以代码为例):

public class MyAdapter extends BaseAdapter implements SectionIndexer {

    private List<Student> students ;
    private Context context ;

    public MyAdapter(List<Student> students, Context context) {
        this.context = context;
        setData(students) ;
    }

    private void setData(List<Student> students) {
        if(students == null) {
            students = new ArrayList<Student>() ;
        }
        this.students = students ;
    }

    @Override
    public int getCount() {
        return students.size() ;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder ;
        if(convertView == null) {
            holder = new ViewHolder() ;
            convertView = LayoutInflater.from(context).inflate(R.layout.listview_item, null) ;

            holder.tvName = (TextView) convertView.findViewById(R.id.tv_item_name) ;
            holder.tvSortKey = (TextView) convertView.findViewById(R.id.tv_item_sortKey) ;

            convertView.setTag(holder) ;
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        Student student = students.get(position) ;

        holder.tvName.setText(student.getName()) ;
        holder.tvSortKey.setText(getFirstChar(student.getSortKey())) ;

        // 使用SectionIndex的解决方案:判断是否显示首字母
        // 1. 获取当前Student的sortKey的首字母
        int section = getSectionForPosition(position) ;
        // 2. 获取当前首字母应该出现的位置
        int sectionPos = getPositionForSection(section) ;
        // 3. 判断当前getView()时的position,是否与当前section应该出现的位置相符
        if(position == sectionPos) {
            holder.tvSortKey.setVisibility(View.VISIBLE) ;
        } else {
            holder.tvSortKey.setVisibility(View.GONE) ;
        }

        return convertView ;
    }

    private class ViewHolder {

        private TextView tvName ;
        private TextView tvSortKey ;

    }

    /**
     * 获取当前位置的首字母
     * @param sortKey
     * @return
     *      当前位置的首字母
     */
    private CharSequence getFirstChar(String sortKey) {
        return sortKey.substring(0, 1).toUpperCase(Locale.CHINA) ;
    }

    @Override
    public int getPositionForSection(int section) {
        // 为首字母获取位置,即:根据首字母确定该首字母在数据集中的位置
        int position = 0 ;
        for(int i = 0; i < students.size(); i++) {
            // 根据当前循环到的i,获取对应的首字母
            int currentChar = getSectionForPosition(i) ;
            // 判断当次循环到的首字母是否与参数相等,如果相等,则可以确定参数对应的字母应该出现的位置
            if(currentChar == section) {
                position = i ;
                break ;
            }
        }
        return position ;
    }

    @Override
    public int getSectionForPosition(int position) {
        // 获取当前位置的首字母
        return students.get(position).getSortKey().toUpperCase(Locale.CHINA).charAt(0) ;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public Object[] getSections() {
        return new Object[0];
    }

}

    具体的实体类和数据的操作类我在这里就不贴代码了,不要在这里跟我讲你还不会获取数据。

    当看完这篇文字你是否发现,其实自定义控件并不是那么难,对于我们想要实现的控件,只要我们能想得到,我们就可以把它画出来,而且也可以将一些其他的逻辑处理全部封装在我们的自定义类里面,就像本例中的字母的点击监听逻辑,我们也可以在这里为其设置其他更多的逻辑,当你需要使用的时候,直接调用即可。

    最后总结一下自定义控件的实现步骤:

    1、自定义类继承View类,并实现其三个构造方法;

    2、重写onDraw()方法,绘制自己需要绘制的控件;

    3、可根据需要添加一些其他处理逻辑(如点击变化、滑动变化等一系列效果);

    4、在布局文件中声明该控件;

    5、在需要显示的界面初始化,并为其添加需要实现的逻辑;

    希望这篇文字能够对大家有所帮助,其实自定义控件这个知识点是相当大的,不是一篇博客能够解释清楚,具体就需要大家在平时的使用中去探索了,另外,我下面为大家附上一个链接,大家有兴趣的可以参考一下爱哥的自定义控件的博客。

    http://blog.csdn.net/aigestudio?viewmode=contents

发布了32 篇原创文章 · 获赞 14 · 访问量 6万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章