一、自定义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需要多多练习才行。