AndroidUI之如何自定义控件

来源:互联网 发布:北电网络 编辑:程序博客网 时间:2024/06/05 22:58

前言

  对Android开发者而言,学习到一定时候,就会发现Android自带的控件诸如TextView,Button,ImageView,配合上布局如LinearLayout, RelativeLayout,TableLayout,FrameLayout等,做不出自己想要的效果。这个时候就要用到自定义控件。

  • 自定义空间的有点有很多,包括:
    • 用户交互体验的优化;
    • 在大数据量情况下自定义控件比写布局效率高
    • 布局上更加符合需求;
    • 能配合App整体UI风格的设计;
    • 等等

现在网上有许多开源的自定义控件共享,供开发者使用。而我们在实际情况中也会遇到需要自定义控件的时候,今天我们就来一起学习如何自定义控件。
  
  (PS:本文内容基于官方教程进行学习解析,若想查看原文请点击阅读。
  参考博客:guolin博文&博文,大家可以看看,受益匪浅)

View

  View是我们平时所见到的所有视图的父类,每个布局,控件,都是直接或者间接继承自View。一个View在屏幕上显示出来要涉及到三个函数measure(),layout()和draw()。

  • 它们的作用分别是:

    • measure()测量视图布局大小
    • layout()设置视图在屏幕中的位置
    • draw()将视图显示在屏幕上
  • Measure:

    • 调用measure()测量
    • measure()中调用onMeasure(),onMeasure()是实际测量视图大小的函数
    • onMeasure()中调用setMeasureDimension()保存测量结果;
  • Layout:

    • layout调用setFrame(l,t,r,b),l,t,r,b即left, top, right, bottom,是与父View的距离。setFrame()会判断这个视图大小有没有改变来决定是否重绘。然后调用onLayout()
    • onLayout()是个空方法由子类去实现,如TextView继承自View里面就对这个方法进行了重写。
    • 特别一提,onLayout()在ViewGroup中是个抽象方法,没有进行实现。意思是在如果你创建了一个View继承自ViewGroup,则必须重写onLayout()方法。
  • Draw:

    • draw会在一个canvas对象上进行绘制,然后显示在屏幕上
    • 和上面两个类似,draw()中也有实际上负责绘制逻辑的函数onDraw(),诸如TextView,ImageView中有自己重写的onDraw()函数。
    • onDraw()通过对传入的canvas对象进行逻辑处理,得到所要显示的样子。

自定义控件

知道了View的绘制流程之后,我们开始学习怎么去自定义一个控件。

  • 自定义控件按照对View及其子类的依赖程度,可以分为:
    • 组合型控件。利用现有控件的组合实现更加复杂的布局。
    • 继承型控件。继承现有的View或者View的子类,如TextView,ImageView等,并在此基础上增加一点内容。
    • 自创型控件。利用上面说的View绘制原理,自行设计绘制出来的全部内容并加以实现。

组合型控件:

  先是在layout中创建一个布局文件,如例子中是一个Button和一个TextView,view_layout.xml,实现类似标题栏的效果:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="50dp"    android:background="#ff0000" >    <Button        android:background="#00ff00"        android:id="@+id/button"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_margin="5dp"        android:textSize="18sp"        android:text="Button"        android:textColor="#0000ff" />    <TextView        android:id="@+id/text"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:text="TextView"        android:textColor="#0000ff"        android:textSize="22sp" /></RelativeLayout>

  然后是控件对应的类,在里面实现逻辑处理,CombView.java

import android.content.Context;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.View;import android.widget.Button;import android.widget.RelativeLayout;import android.widget.TextView;import com.example.wujiayi.testapplication.R;public class CombView extends RelativeLayout{    private TextView textView;    private Button button;    private int count;    public CombView(Context context, AttributeSet attrs) {        super(context, attrs);        //加载视图的布局        LayoutInflater.from(context).inflate(R.layout.view_layout, this);        textView = findViewById(R.id.text);        button = findViewById(R.id.button);        button.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View view) {                textView.setText(Integer.toString(++count));            }        });    }}

  例子比较简单,我就不多做介绍了。在写完布局文件和逻辑实现之后,就能开始使用了,activity_main.xml

<LinearLayout 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"    android:orientation="vertical">    <com.example.testapplication.views.CombView        android:layout_width="match_parent"        android:layout_height="60dp"/></LinearLayout>

  最后得到的效果图如下:


  在点击后TextView字样会变成数字并从1开始计数:



继承型控件:

  继承型控件我们要做的是一个MyListview,滑动时在上面显示删除按钮,点击删除按钮时删除该条目。老规矩先创建一个布局文件,delete_btn.xml:

<?xml version="1.0" encoding="utf-8"?><Button xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/delete_button"    android:text="删除"    android:layout_width="wrap_content"    android:layout_height="wrap_content"></Button>

  然后是自定义的View的代码,监听手势,逻辑如下所示,也比较简单就不进行解释了,MyListView.java:
  

//继承自ListView,同时实现OnTouchListener和OnGestureListener接口public class MyListView extends ListView implements View.OnTouchListener,        GestureDetector.OnGestureListener {    private View delete_btn;    private ViewGroup listView;    private int seletedItem;    private boolean isDeleteshown;    private GestureDetector gestureDetector;    private OnDeleteListener listener;    public MyListView(Context context, AttributeSet attrs) {        super(context, attrs);        gestureDetector = new GestureDetector(getContext(), this);        setOnTouchListener(this);    }    public void setOnDeleteListener(OnDeleteListener _listener) {        listener = _listener;    }    @Override    public boolean onTouch(View view, MotionEvent motionEvent) {        if (isDeleteshown) {            listView.removeView(delete_btn);            delete_btn = null;            isDeleteshown = false;            return false;        }else {            return gestureDetector.onTouchEvent(motionEvent);        }    }    @Override    public boolean onDown(MotionEvent motionEvent) {        if (!isDeleteshown) {            seletedItem = pointToPosition((int) motionEvent.getX(), (int) motionEvent.getY());        }        return false;    }    @Override    public void onShowPress(MotionEvent motionEvent) {    }    @Override    public boolean onSingleTapUp(MotionEvent motionEvent) {        return false;    }    @Override    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {        return false;    }    @Override    public void onLongPress(MotionEvent motionEvent) {    }    @Override    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,                           float velocityY) {        if (!isDeleteshown && Math.abs(velocityX) > Math.abs(velocityY)) {            delete_btn = LayoutInflater.from(getContext()).inflate(                    R.layout.delete_btn, null);            delete_btn.setOnClickListener(new OnClickListener() {                @Override                public void onClick(View v) {                    listView.removeView(delete_btn);                    delete_btn = null;                    isDeleteshown = false;                    listener.onDelete(seletedItem);                }            });            listView = (ViewGroup) getChildAt(seletedItem                    - getFirstVisiblePosition());            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);            params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);            params.addRule(RelativeLayout.CENTER_VERTICAL);            listView.addView(delete_btn, params);            isDeleteshown = true;        }        return false;    }    public interface OnDeleteListener {        void onDelete(int index);    }

  到此为止,我们继承自ListVeiw的自定义控件就完成了。下面是使用这个自定义控件的示例,先是创建一个ListView的子项布局文件listview_item.xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:descendantFocusability="blocksDescendants"    android:orientation="vertical" >    <TextView        android:id="@+id/listview"        android:layout_width="wrap_content"        android:layout_height="50dp"        android:layout_centerVertical="true"        android:gravity="start|center_vertical"        android:textColor="#000" /></RelativeLayout>

  布局完之后,用一个adapter去加载刚刚写的的布局,MyAdapter.java:

public class MyAdapter extends ArrayAdapter<String>{    public MyAdapter(Context context, int textViewResourceId, List<String> objects) {        super(context, textViewResourceId, objects);    }    @NonNull    @Override    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {        View view;        if (convertView == null) {            view = LayoutInflater.from(getContext()).inflate(R.layout.listview_item, null);        } else{            view = convertView;        }        TextView textView = view.findViewById(R.id.listview);        textView.setText(getItem(position));        return view;    }}

  子项布局和加载都完成了以后,就可以在activity_main.xml中引入这个布局了:
  

<LinearLayout 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"android:orientation="vertical">    <com.example.testapplication.views.MyListView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:id="@+id/my_listview"/></LinearLayout>

  布局完成,在MainActivity.java中进行点击删除按钮的逻辑实现:

public class MainActivity extends AppCompatActivity {    private MyListView listView;    private MyAdapter adapter;    private List<String> contentList = new ArrayList<String>();    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initList();        listView = (MyListView) findViewById(R.id.my_listview);        listView.setOnDeleteListener(new MyListView.OnDeleteListener() {            @Override            public void onDelete(int index) {                contentList.remove(index);                adapter.notifyDataSetChanged();            }        });        adapter = new MyAdapter(this, 0, contentList);        listView.setAdapter(adapter);    }    private void initList() {        for (int i = 0; i < 10; i++) {            contentList.add("Item" + Integer.toString(i));        }    }}

  一起来看看效果吧:
  子项中滑动,出现删除按钮


  点击删除后


自创型控件:

  自创型控件我们做的是一个圆,类似于统计图中的饼状图。由于是自绘的,所以不存在已有控件布局问题,我们直接从measure,layout,draw三步入手,下面给出TestView.java:
  

public class TestView extends View {    private Paint mPaint;    private RectF oval;    public TestView(Context context) {        super(context);        initView();    }    public TestView(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView(){        mPaint = new Paint();        mPaint.setAntiAlias(true);        oval=new RectF();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        switch (widthMode) {            case MeasureSpec.EXACTLY:                break;            case MeasureSpec.AT_MOST:                break;            case MeasureSpec.UNSPECIFIED:                break;        }    }    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        super.onLayout(changed, left, top, right, bottom);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        mPaint.setColor(Color.GRAY);        // FILL填充, STROKE描边,FILL_AND_STROKE填充和描边        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);        int with = getWidth();        int height = getHeight();        float radius = with / 4;        canvas.drawCircle(with / 2, with / 2, radius, mPaint);        mPaint.setColor(Color.BLUE);        oval.set(with / 2 - radius, with / 2 - radius, with / 2                + radius, with / 2 + radius);//用于定义的圆弧的形状和大小的界限        canvas.drawArc(oval, 270, 120, true, mPaint);  //根据进度画圆弧    }}

  主要实现的步骤是在canvas上画了一个圆形,需要重写onDraw()函数。有时候会需要重写onMeasure()和onLayout(),视实际情况而定。
  在写完一个自创型控件之后,我们需要在布局中引用,activity_main.xml:
  

<LinearLayout 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"android:orientation="vertical">    <com.example.testapplication.views.TestView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_margin="10dp" /></LinearLayout>

  我们一起来看看效果图:


总结

  好了,这篇文章就先写到这里。我们来回顾一下全文内容:
  我们首先分析了View在屏幕上显示出来需要的步骤包括measure,layout和draw。
  然后一起学习了三种类型的控件,包括组合型,继承型和自创型,并在其中运用了上面所学的View显示的知识。
  最后,自定义控件中还要考虑自定义属性的问题,我们会在以后的博文中再一起学习这一点。
  文章是自己的一点浅陋的理解,如有不当之处欢迎指出,谢谢观看。

原创粉丝点击