androidP: View的工作原理-自定义View

一、自定义View分类
1、集成View重写onDraw
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

2、集成ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,即除了LinerLayout \RelativeLayout\FrameLayout这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量,布局这两个过程,并同时处理子元素的测量和布局过程。

3、继承特定的View(比如TextView)
这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView。这种方法不需要自己支持wrap_content和padding等。

4、继承特定的ViewGroup(比如LinearLayout)
当某种效果看起来很像集中View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。

二、自定义View须知
1、让View支持wrap_content
这是因为直接集成VIew或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。

2、如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

3、尽量不要再View中使用Handler,没必要
这是因为View内部本身就提供了post系列方法,完全可以替换Handler的作用,当然除非你很明确地要使用Handler来发送消息。

4、View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
如果有线程或者动画需要停止,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,VIew的onAttachedToWindow方法会被调用。同时,当View变得不可见时,也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。

5、View带有滑动嵌套时,需要处理好滑动冲突
如果有滑动冲突的话,那么要合适地处理冲突,否则将会严重影响View的效果。(处理方法后面学习)

三、自定义View示例
1、继承View。自定义圆,注意在onMeaure中处理wrap_content情况;在onDraw方法处理padding。完整代码:

//activity.java
public class showCustomViewActivity extends AppCompatActivity {

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

    @Override
    protected void onStart() {
        super.onStart();
    }
}

//activity.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">

    <com.example.service.view.CustomView
        android:id="@+id/customview"
        android:layout_height="150dp"
        android:layout_width="100dp"
        android:layout_marginLeft="200dp"
        android:paddingTop="100dp"/>

</RelativeLayout>

//CustomView.java
package com.example.service.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class CustomView extends View {
    private Paint mPaint;
    public CustomView(Context context) {
        super(context);
        init();
    }
    
    public CustomView(Context context, AttributeSet attrs) {
        super(context,attrs);
        init();
    }

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


    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.GRAY);
    }

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

        //处理wrap_content
        int width = 100;
        int height = 150;
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,height);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width,heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize,height);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //处理padding
        int paddingl = getPaddingLeft();
        int paddingt = getPaddingTop();
        int paddingr = getPaddingRight();
        int paddingb = getPaddingBottom();

        int width = getWidth() - paddingl - paddingr;
        int height = getHeight() - paddingt - paddingb;
        float x = width  * 0.5f + paddingl;
        float y = height * 0.5f + paddingt;
        float r = Math.min(width * 0.5f,height * 0.5f);
        canvas.drawCircle(x,y,r,mPaint);
    }
}

相关知识点:
1)margin与padding的区别
在这里插入图片描述
padding:内边距。指View中的内容与该View边缘的距离。
margin:外边距。指View本身与其他VIew或父View的距离。

2)自定义View中的三种构造方法。
public View (Context context)是在java代码创建视图的时候被调用,如果是从xml填充的视图,就不会调用这个,在代码里new的话一般用一个参数的构造函数。
public View (Context context, AttributeSet attrs)这个是在xml创建但是没有指定style的时候被调用。参数AttributeSet记录view在xml布局中的属性(本例属于这种情况)。
public View (Context context, AttributeSet attrs, int defStyle)这个是在xml创建,指定style的时候被调用,
第三个函数系统是不调用的,要由View(我们自定义的或系统预定义的View)显式调用,比如在第二个构造函数中调用了第三个构造函数,并将R.attr.CustomizeStyle传给了第三个参数。第三个参数的意义就如同它的名字所说的,是默认的Style,只是这里没有说清楚,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style。

补充知识:
为了让我们的View更加容易使用,很多情况下我们还需要为了其提供自定义属性:
步骤一:在values目录下创建自定义属性的xml,比如attrs.xml,该文件名字最好以attrs开头,例如 attrs_circle_view.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

上面的xml中声明了一个自定义属性集合“CircleView”,在这个集合里面可以有很多自定义属性,这里只定义了一个格式为"color"的属性"circle_color"。

步骤二:在View的构造方法中解析自定义属性的值并做相应处理。比例,对“circle_color”属性处理;

private Paint mPaint;
private int mColor;
public CustomView(Context context, AttributeSet attrs) {
    super(context,attrs);
    Log.d(TAG," =================>CustomView = 2");
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
    mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
    a.recycle();
    init();
}
private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(mColor);
}

首先加载自定义属性集合CircleView,接着解析CircleView属性集合中的circle_color属性,它的id为R.styleable.CircleView_circle_color。如果没有设定颜色,默认颜色为红色,解析完自定义属性后,通过recycle方法来释放资源。

步骤三:在布局文件中使用自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.service.view.CustomView
        android:id="@+id/customview"
        android:layout_height="150dp"
        android:layout_width="100dp"
        android:layout_marginLeft="200dp"
        android:paddingTop="20dp"
        app:circle_color="#ff5678"/>

</RelativeLayout>

在布局文件中使用自定义属性时,必须在布局文件中添加schemas声明:“xmlns:app="http://schemas.android.com/apk/res-auto”。在这个声明中,app是自定义属性前缀(可以换为其他名字),例如:
在这里插入图片描述
结束。

2、继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,需要合适地处理ViewGroup的测量(onMeasure)/布局(onLayout)这两个过程,并同时处理子元素的测量和布局过程。
自定义FlowLayout流式布局。代码:

//activity.java
package com.example.service.view;

import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.example.service.R;

import java.util.ArrayList;
import java.util.List;

public class showCustomFlowLayoutActivity extends AppCompatActivity {
    private List<String> mContent;
    private CustomFlowLayout mCustomFlowLayout;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_show_custom_flowlayout);
    }

    @Override
    protected void onResume() {
        super.onResume();
        initViews();
        initParas();

    }

    private void initViews() {
        mCustomFlowLayout = findViewById(R.id.fl_my);
    }

    private void initParas() {
        mContent = new ArrayList<String>();
        mContent.add("nfannojoanf djaiohfa");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjd");
        mContent.add("nfannojoan");
        mContent.add("nfannojoanfdjaio");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr11111111");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr23edfddddfafafafa");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrrhfhhfdhdfhahdfhahfafhffahfo111111199999");
        mContent.add("nfannojoanf djaiohfa");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjd");
        mContent.add("nfannojoan");
        mContent.add("nfannojoanfdjaio");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr11111111");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrr23edfddddfafafafa");
        mContent.add("nfannojoanfdjaiohfadhhdhddhjddsdfrrhfhhfdhdfhahdfhahfafhffahfo111111199999");
//        mCustomFlowLayout.initData(mContent);

        //在代码中加载textview
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        if (mCustomFlowLayout != null) {
            mCustomFlowLayout.removeAllViews();
        }

        for (int i = 0; i < mContent.size();i ++) {
            TextView textview = new TextView(this);
            textview.setText(mContent.get(i));
            textview.setBackgroundResource(R.drawable.textview_background);
            textview.setPadding(10,10,10,10);
            textview.setTextColor(Color.parseColor("#8b7500"));
            textview.setLayoutParams(layoutParams);
            mCustomFlowLayout.addView(textview);
        }
    }
}

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

    <com.example.service.view.CustomFlowLayout
        android:id="@+id/fl_my"
        android:background="#de4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

//自定义FlowLayout布局 CustomFlowLayout.java
package com.example.service.view;

import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.RequiresApi;

import com.example.service.R;

import java.util.ArrayList;
import java.util.List;


public class CustomFlowLayout extends ViewGroup {
    private final String TAG = "FlowLayout";

    private Context mContext;
    private List<String> mContents;

    //默认FlowLayout的布局大小
    private int mWidth = 1300;
    private int mHeight = 500;

    //孩子位置坐标
    private int mMarginTop = 10;
    private int mMarginLeft = 5;
    private int mChildLeft = 0;
    private int mChildTop = 0;
    private int mChildRight = 0;
    private int mChildBottom = 0;

    public CustomFlowLayout(Context context) {
        super(context);
        Log.d(TAG,"==================>CustomFlowLayout 1");
        mContext = context;
    }

    public CustomFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.d(TAG,"==================>CustomFlowLayout 2");
        mContext = context;
    }

    public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.d(TAG,"==================>CustomFlowLayout 3");
        mContext = context;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.d(TAG,"==================>CustomFlowLayout 4");
        mContext = context;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG,"==================>onMeasure()");
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        Log.d(TAG,"mWidthSize = " + widthSize + ",mHeightSize = " + heightSize);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            Log.d(TAG,"==================>mWidthMode == MeasureSpec.AT_MOST,mHeightMode == MeasureSpec.AT_MOST");
            setMeasuredDimension(mWidth,mHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            Log.d(TAG,"==================>mWidthMode == MeasureSpec.AT_MOST");
            setMeasuredDimension(mWidth,heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            Log.d(TAG,"==================>mHeightMode == MeasureSpec.AT_MOST");
            setMeasuredDimension(widthSize,mHeight);
        }

        //孩子
        int count = getChildCount();
        Log.d(TAG,"onMeasure child : count = "+ count);
        for (int i = 0; i < count; i ++) {
            View child = getChildAt(i);
            //测量孩子的宽/高
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            Log.d(TAG,"onMeasure child : width = " + child.getMeasuredWidth()  + ",height = " + child.getMeasuredHeight());
        }


    }

    //这里的l,t,r,b是flowlayout的坐标
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG,"==================>onLayout" + " changed = " + changed + ",l = " + l + ",t = " + t + ",r = " + r + ",b = " + b);

        //FlowLayout 宽高
        int widthFlowLayout = getMeasuredWidth();
        int heightFlowLayout = getMeasuredHeight();
        Log.d(TAG,"==================>widthFlowLayout = " + widthFlowLayout + ",heightFlowLayout = " + heightFlowLayout);

        mChildLeft =  mMarginLeft;
        mChildTop = mMarginTop;
        int count = getChildCount();

        for (int i = 0; i < count; i ++) {
            View child = getChildAt(i);

            //获取孩子测量的宽/高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            mChildRight = mChildLeft + childWidth;
            mChildBottom = mChildTop + childHeight;

            if (mChildRight + l >= r) {
                mChildLeft =  mMarginLeft;
                mChildTop = mMarginTop + mChildBottom;
                mChildRight = mChildLeft + childWidth;
                mChildBottom = mChildTop + childHeight;
            }
            child.layout(mChildLeft,mChildTop,mChildRight,mChildBottom);
            Log.d(TAG,"onLayout child : mChildLeft = " + mChildLeft + ",mChildTop = " + mChildTop + ", mChildRight = " + mChildRight + ",mChildBottom = " + mChildBottom);

            mChildLeft = mChildRight + mMarginLeft;
        }
    }


    public void initData(List<String> content) {
        mContents = content;
        int count = (mContents == null) ? 0 : mContents.size();
        for (int i = 0; i < count; i ++) {
            View view = LayoutInflater.from(mContext).inflate(R.layout.flowlayout_text,null,false);
            TextView textview = view.findViewById(R.id.tv_flowlayout_text);
            textview.setText(mContents.get(i));
            textview.setBackgroundResource(R.drawable.textview_background);
            textview.setPadding(10,10,10,10);
            textview.setTextColor(Color.parseColor("#12d2fd"));
            addView(view);
        }
    }
}

//使用布局形式向flowLayout中添加子view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_flowlayout_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

四、自定义view的思想
首先要掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等等。这些东西都是自定义View所必须的,尤其是那些看起来很炫的自定义view,他们往往对这些技术点要求更高;熟练掌握基本功以后,在面对新的自定义View时,要能够对其分类并选择合适的实现思路。另外平时还需要多积累一些自定义View的相关经验。
就这样吧,感觉自定义view需要多多练习才行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值