Android custom control (View)

        The Android system provides us with many native controls, but in some cases the native controls of the system cannot meet our needs well. At this time, we need to use custom controls, and custom controls can also achieve a lot of cool Effect.

       Divided by type, the implementation of custom controls can basically be divided into three types, self-drawn controls, combined controls, and inherited controls.

  1. Self-drawing control: All the content displayed on this control is drawn by yourself, which is different from the native controls of the system. Many cool custom control effects are mostly of this kind;
  2. Combination controls: Sometimes the application interface needs to repeatedly combine several native controls together. At this time, we can combine these controls into one control, which is convenient to use. At this time, we don’t need to draw the display on the view by ourselves. content;
  3. Inherited controls: Sometimes the system's native controls can meet most of our needs but cannot meet some special requirements. At this time, we don't need to implement a control from scratch by ourselves. We only need to inherit the native control and extend our needs based on it. The function can be.

        〇, View drawing process

        All controls in Android are directly or indirectly inherited from View. The display of any control View must be drawn by the system. There are three main stages in the process of drawing View by the system: 1. The measure method determines the control. Size; 2. The layout method determines the position of the control; 3. The draw method draws the content of the control.

    //measure方法
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
    //layout方法
    public void layout(int l, int t, int r, int b) {
        ......
        onLayout(changed, l, t, r, b);
        ......
    }
    //draw方法
    public void draw(Canvas canvas) {
        ......
        if (!dirtyOpaque) onDraw(canvas);
        ......
    }

        These three methods call the onMeasure(), onLayout(), and onDraw() methods respectively; when we need to customize the control, we need to rewrite one or more of these three methods according to the function to perform our own drawing logic: Determine the size of the control in the onMeasure() method, determine the position of the control in onLayout(), and draw the content of the control in the onDraw() method:

package com.mliuxb.mycustomview0328;

import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Description: View的绘制流程
 */
public class CustomView extends View {
    private static final String TAG = "CustomView";

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

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.i(TAG, "onMeasure: ");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.i(TAG, "onLayout: ");
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Log.i(TAG, "onDraw: ");
        super.onDraw(canvas);
    }
}

        One, self-drawing control

        Draw a simple ring control by yourself, the effect is as follows:

        First, you need to write a MyRing class to inherit View, and implement the construction method, initialize in the construction method, and then draw in the onDraw() method.

package com.mliuxb.mycustomview0328;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Description: 自定义圆环
 */
public class MyRing extends View{
    private static final String TAG = "MyRing";

    private Paint paint;

    public MyRing(Context context) {
        this(context, null);
    }

    public MyRing(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }
    
    private void init() {
        //创建画笔
        paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);//空心
        paint.setStrokeWidth(20); //设置圆环宽度
        paint.setAntiAlias(true); //消除锯齿
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i(TAG, "onDraw: ");
        //绘制圆形:参1,2: 圆心坐标; 参3:半径
        canvas.drawCircle(getWidth()/2,getHeight()/2, 200, paint);
    }
}

        In this way, the customized ring control is completed, and then let the ring be displayed on the interface, and add the ring control in the layout file (note that the full class name should be used)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.mliuxb.mycustomview0328.MyRing
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</android.support.constraint.ConstraintLayout>

        In this way, the ring control can be displayed on the main page, as shown in the above rendering. Such a simple self-drawing control is complete.

        Second, the combination control

        As shown in the figure below, you must be familiar with similar entry layouts. It is very common in the personal information page or settings page of the application. We need to repeatedly combine several native controls to use:

    

        At this time, we can combine these several controls into one control (custom combination control), which is convenient to use.

        First, create a new item layout file view_item.xml according to the requirements of the item:

<?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="60dp"
    android:padding="15dp">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"/>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginStart="10dp"
        android:layout_toEndOf="@+id/iv_image"
        android:textColor="#3C3C3C"
        android:textSize="14sp"/>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:src="@drawable/more_arrow"/>
</RelativeLayout>

        As above, take the RelativeLayout layout as the root layout, including an ImageView, TextView, and one more arrow ImageView. Then you need to create a custom control ItemView and inherit from FrameLayout, and implement the construction method, initialize it in the construction method, the code is as follows:

package com.mliuxb.mycustomview0328;

import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * Description:自定义组合控件
 */
public class ItemView extends FrameLayout {
    private static final String TAG = "ItemView";

    private ImageView ivImage;
    private TextView  tvTitle;

    public ItemView(@NonNull Context context) {
        this(context, null);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        initItemView(context);
    }

    private void initItemView(@NonNull Context context) {
        //加载布局,注意参二传入this对象,表示指定当前的 ItemView 对象为加载的布局文件的父控件
        LayoutInflater.from(context).inflate(R.layout.view_item, this);
        ivImage = findViewById(R.id.iv_image);
        tvTitle = findViewById(R.id.tv_title);
    }

    public void setTitle(CharSequence text) {
        tvTitle.setText(text);
    }


    public void setImageResource(@DrawableRes int resId) {
        ivImage.setImageResource(resId);
    }

}

        In the initialization method, first we use the inflate() method of the layout filler LayoutInflater to load the view_item.xml layout just defined, and pay special attention to [parameter two] passing in a  this  object, which means that the current ItemView object is specified as The parent control of the loaded layout file, that is, view_item.xml is laid out as the child layout of ItemView. Otherwise, if null  is passed  in, we need to use the addView(child) method to add a child layout to the current ItemView.

        Then use the findViewById() method to get the control, and provide the setTitle() and setImageResource() methods to set the image and title of each item externally, so that the ItemView control can be referenced in the layout file of the page, and in the code Set unused content for each item in the, so a custom combination control comes to an end.

        Now we can set unused content (pictures and titles) for each item in the code, but if we want to control the content of each item directly in the ItemView tag of the page layout file, just like the various properties of system controls, this You need to customize some properties for the ItemView control .

        First create the attrs.xml attribute file in the res/values ​​directory, and then follow the format in the system’s attrs.xml attribute file to customize the attributes of the ItemView control, as shown below:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!--自定义ItemView的属性-->
    <declare-styleable name="ItemView">

        <!--条目的标题,格式为string-->
        <attr name="item_title" format="string"/>

        <!--条目的图片,格式为drawable-->
        <attr name="item_image" format="reference"/>
    </declare-styleable>

</resources>

        We know that when we set the attributes for the control in the layout file, when the system creates the control at the bottom, it will pass the values ​​of all the attributes in the layout file through the AttributeSet parameter in the construction method, and the custom attributes are also the same , So we need to do the corresponding operation when the value of the custom attribute is passed, so we add the  initAttributes() method to the ItemView to process the relevant data of the custom attribute:

package com.mliuxb.mycustomview0328;

import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * Description:自定义组合控件
 */
public class ItemView extends FrameLayout {
    private static final String TAG = "ItemView";
    private static final String NAME_SPACE = "http://schemas.android.com/apk/res-auto";

    private ImageView ivImage;
    private TextView  tvTitle;

    public ItemView(@NonNull Context context) {
        this(context, null);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ItemView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        initItemView(context);
        if (attrs != null) {
            initAttributes(attrs);
        }
    }

    private void initItemView(@NonNull Context context) {
        //加载布局,注意参二传入this对象,表示指定当前的 ItemView 对象为加载的布局文件的父控件
        LayoutInflater.from(context).inflate(R.layout.view_item, this);
        ivImage = findViewById(R.id.iv_image);
        tvTitle = findViewById(R.id.tv_title);
    }

    public void setTitle(CharSequence text) {
        tvTitle.setText(text);
    }

    public void setImageResource(@DrawableRes int resId) {
        ivImage.setImageResource(resId);
    }

    //初始化自定义属性
    private void initAttributes(@NonNull AttributeSet attrs) {
        //从AttributeSet中取出自定义的属性
        String itemTitle = attrs.getAttributeValue(NAME_SPACE, "item_title");
        int itemImage = attrs.getAttributeResourceValue(NAME_SPACE, "item_image", 0);

        //根据自定义属性的值更新相关控件
        setTitle(itemTitle);
        setImageResource(itemImage);
    }
}

        At this point, we can directly reference the custom combination control ItemView in the layout page, and set different properties for the control in the layout file. The layout file is as follows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <include layout="@layout/layout_line_10dp"/>

    <com.mliuxb.mycustomview0328.ItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_image="@drawable/my1_message"
        app:item_title="我的消息"/>

    <include layout="@layout/layout_line_1dp"/>

    <com.mliuxb.mycustomview0328.ItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_image="@drawable/my2_learn"
        app:item_title="我的学习"/>

    <include layout="@layout/layout_line_1dp"/>

    <com.mliuxb.mycustomview0328.ItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_image="@drawable/my3_member"
        app:item_title="我的会员"/>

    <include layout="@layout/layout_line_1dp"/>

    <com.mliuxb.mycustomview0328.ItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_image="@drawable/my4_gold"
        app:item_title="我的金币"/>

    <include layout="@layout/layout_line_10dp"/>
</LinearLayout>

        At this time, the content of the entry is not set in the code, and the custom attributes of the layout file have taken effect, as shown below. The current interface looks the same as the original four RelativeLayout layouts, but our processing method is completely different. If there are many such items, the subsequent use will be very convenient, and the layout files are organized. It will be very clear.

        Three, inherit the control

        The characteristic of the inherited control is that it can not only add the corresponding functions according to our needs, but also retain all the functions of the native control, so in actual development, this kind of custom control should be the most frequently used by us.

        For example, with regard to the input box control, the right side of the iOS input box comes with a delete button. After clicking it, one click deletes the content of the input box; Android's EditText does not have corresponding functions. We can also use RelativeLayout to wrap an EditText and ImageView, and add text change monitoring for EditText in the code and set the click event of ImageView to achieve the corresponding function. This is simple and convenient, but each input box needs such a process. If there are more input boxes, just delete this one-click The function will occupy a lot of code. At this point, we can consider implementing a custom control that inherits EditText to extend this function for EditText.

        The realization idea is to first customize the ClearEditText class to inherit AppCompatEditText (Studio recommends inheriting AppCompatEditText instead of EditText), so as to ensure that our ClearEditText input box retains all the functions of the native EditText, and then implement the construction method, and add text change monitoring for ClearEditText And set the click event of the ImageView, and clear the content of the input box when the delete icon is clicked, the code is as follows:

package com.mliuxb.mycustomview0328;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * Description:自定义带删除功能的ClearEditText
 */
public class ClearEditText extends AppCompatEditText {
    private static final String TAG = "ClearEditText";

    public ClearEditText(Context context) {
        this(context, null);
    }

    public ClearEditText(Context context, AttributeSet attrs) {
        //这个构造方法也很重要,xml中的属性值通过AttributeSet参数传递过来。
        this(context, attrs, android.support.v7.appcompat.R.attr.editTextStyle);
    }

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

    private void initView() {
        //获取clear图片的Drawable对象
        final Drawable drawable = getResources().getDrawable(R.mipmap.clear, null);
        //添加监听
        addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (TextUtils.isEmpty(s)) {
                    setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
                } else {
                    //drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
                    setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
                }
            }
        });
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取rightDrawable
        Drawable rightDrawable = getCompoundDrawables()[2];
        if (event.getAction() == MotionEvent.ACTION_UP && rightDrawable != null) {
            float eventX = event.getX();//获取点击的X轴坐标
            float eventY = event.getY();//获取点击的Y轴坐标
            int width = getWidth();     //获取控件的宽度
            int height = getHeight();   //获取控件的高度
            int paddingEnd = getPaddingEnd();//获取右内边距
            int intrinsicWidth = rightDrawable.getIntrinsicWidth();  //获取rightDrawable的固有宽度
            int intrinsicHeight = rightDrawable.getIntrinsicHeight();//获取rightDrawable的固有高度

            //判断点击位置的X轴范围
            boolean touchedX = (eventX > (width - paddingEnd - intrinsicWidth)) && (eventX < (width - paddingEnd));
            //判断点击位置的Y轴范围
            boolean touchedY = (eventY > (height - intrinsicHeight) / 2) && (eventY < (height + intrinsicHeight) / 2);

            //清除文本
            if (touchedX && touchedY)
                setText("");
        }
        return super.onTouchEvent(event);
    }
}

        First you can see: Get the Drawable object in the initialization method, and add the listener addTextChangedListener() for text changes, so that when the text of the input box changes, it will call back the three methods in TextWatcher, and then judge the change in the afterTextChanged(Editable s) method. Whether the following text is empty, set the display and disappearance of the delete icon for it. At this point, we are not calling the setVisibility() method, because the setVisibility() method is for View, but calling the setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom) method to set the icon on the right.

In addition: setCompoundDrawables(left, top, right, bottom) and setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom) two methods can set the upper left and lower right icons for the TextView, the difference lies in the setCompoundDrawables(left, top, right, bottom) method Before calling, you must also call the drawable.setBounds() method to set the width and height of the drawable, but setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom) is not required.

        Then you can see: in the onTouchEvent (MotionEvent event) method to determine the relevant click event, this is because there is no way in Android to let us set the click monitoring event for the icon on the right, so we think that the ACTION_UP (up) event in MotionEvent When it happens, the click has already occurred, and then the position coordinates of the click are judged. If the click coordinates are within the range of the delete icon, it is considered that the delete icon has been clicked, and the text content is cleared at this time.

        Such an input box with one-click delete function is completed, and it can be used in the layout file:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.mliuxb.mycustomview0328.ClearEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp"/>

</android.support.constraint.ConstraintLayout>

 

The above are the basic implementations of the three custom controls: self-drawn controls, combined controls, and inherited controls.

Guess you like

Origin blog.csdn.net/beita08/article/details/88846437