赞
踩
一、自定义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需要多多练习才行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。