一、.首先 在自定義view方面需要 android 最基礎的知識也就是View的繪製流程了
measure、layout、draw的三個執行流程
measure:測量,測量自己有多大,如果是ViewGroup的話會同時測量裏面的子控件的大小
layout:擺放裏面的子控件
draw:繪製 (重寫onDraw)
MeasureSpec:測量規格
int 32位:010111100011100
拿前面兩位當做mode,後面30位當做值。
1.mode:
1) EXACTLY: 精確的。比如給了一個確定的值 20dp(Match_parent)
2) AT_MOST: 根據父容器當前的大小,結合你指定的尺寸參考值來考慮你應該是多大尺寸,需要計算(wrap_content就是屬於這種)
3) UNSPECIFIED: 最多的意思。根據當前的情況,結合你制定的尺寸參考值來考慮,在不超過父容器給你限定的只存的前提下,來測量你的一個恰好的內容尺寸。 用的比較少,一般見於ScrollView,ListView(大小不確定,同時大小還是變的。會通過多次測量才能真正決定好寬高。)
2.value:寬高的值。
經過大量測量以後,最終確定了自己的寬高,需要調用:setMeasuredDimension(w,h)
寫自定義控件的時候,我們要去獲得自己的寬高來進行一些計算,必須先經過measure,才能獲得到寬高---不是getWidth(),而是getMeasuredWidth()
也就是當我們重寫onMeasure的時候,我們需要在裏面調用child.measure()才能獲取child的寬高。
從規格當中獲取mode和value:
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
反過來將mode和value合成一個規格呢:
MeasureSpec.makeMeasureSpec(resultSize, resultMode);
總結
玩自定義viewGroup需要記住以下幾點 就可以很好的做好自定義控件了
//1.測量自己的尺寸
ViewGroup.onMeasure();
//1.1 爲每一個child計算測量規格信息(MeasureSpec)
ViewGroup.getChildMeasureSpec();
//1.2 將上面測量後的結果,傳給每一個子View,子view測量自己的尺寸
child.measure();
//1.3 子View測量完,ViewGroup就可以拿到這個子View的測量後的尺寸了
child.getChildMeasuredSize();//child.getMeasuredWidth()和child.getMeasuredHeight()
//1.4ViewGroup自己就可以根據自身的情況(Padding等等),來計算自己的尺寸
ViewGroup.calculateSelfSize();
//2.保存自己的尺寸
ViewGroup.setMeasuredDimension(size);
以下案例:
自定義viewGroup
package com.roy.widgetdemo;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
public class CustomLayout extends ViewGroup {
private static int spac = 100;
public CustomLayout(Context context) {
this(context,null);
}
public CustomLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomLayout, defStyleAttr, 0);
for (int i = 0; i < typedArray.length(); i++) {
int index = typedArray.getIndex(i);
switch (index){
case R.styleable.CustomLayout_horizontal_spacing:
spac = typedArray.getDimensionPixelSize(index,spac);
break;
}
}
typedArray.recycle();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int top =0;
int bottom = 0;
int left = 0;
int right =0 ;
int lineFeedIndex =0;
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
if(i!=0){
View previousView = getChildAt(i-1); //上一個view
left = left+ spac + previousView.getMeasuredWidth() ;
}
right = left + childAt.getMeasuredWidth();
if(right>getMeasuredWidth()){
lineFeedIndex = 0;
top = top + childAt.getMeasuredHeight();
left = 0;
}
bottom = top + childAt.getMeasuredHeight();
childAt.layout(left,top,right,bottom); //位置擺放
lineFeedIndex ++ ;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthMeasure = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightMeasure = MeasureSpec.getSize(heightMeasureSpec);
//測量子view
int count = getChildCount(); //獲取子view的個數
for (int i = 0; i < count; i++) {
View view = getChildAt(i); //獲取子view
LayoutParams layoutParams = view.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,0,layoutParams.width); //獲取子view測量規格
int childHeightMeaureSpec = getChildMeasureSpec(heightMeasureSpec,0,layoutParams.height);
view.measure(childWidthMeasureSpec,childHeightMeaureSpec); //測量子view
}
int width =0;
int height =0;
int childCount = getChildCount();
//開始測量自己
switch (widthMode){
case MeasureSpec.EXACTLY:
width = widthMeasure; //父容器多大我就多大
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
width = width+ i*spac + view.getMeasuredWidth();
}
if(width >widthMeasure){
width = widthMeasure;
}
break;
}
switch (heightMode){
case MeasureSpec.EXACTLY:
height = heightMeasure; //父容器多大我就多大
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
height = height + view.getMeasuredHeight();
}
break;
}
setMeasuredDimension(width,height); //保存測量的值
}
public void setAdapter(BaseAdapter baseAdapter){
if(baseAdapter !=null){
for (int i = 0; i < baseAdapter.getCount(); i++) {
View child = baseAdapter.getView(i,null,this);
addView(child);
}
if (getChildCount() > 0) {
requestLayout();
invalidate();
}
}
}
}
attrs文件 在佈局可以通過 以下屬性設置間距
<declare-styleable name="CustomLayout">
<attr name="horizontal_spacing" format="dimension" />
</declare-styleable>
drawable 文件 可以給view設置樣式
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="5dp"/>
<stroke android:color="@color/colorAccent" android:width="2dp"/>
<solid android:color="#fff"/>
</shape>
子view的話就不自己自定義一個了 就利用BaseAdapter類添加view了
public abstract class CommonAdapter<T> extends BaseAdapter{
protected Context mContext;
protected List<T> mDatas;
protected LayoutInflater mInflater;
protected int mLayoutIds;
public CommonAdapter(Context context, List<T> datas, int LayoutIds) {
this.mContext = context;
mInflater = LayoutInflater.from(context);
this.mDatas = datas;
this.mLayoutIds = LayoutIds;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public T getItem(int position) {
return mDatas.get(position);
}
public void refreshData(List<T> datas){
this.mDatas = datas;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent){
ViewHolder holder = ViewHolder.get(mContext, convertView, parent, mLayoutIds, position);
convert(holder, getItem(position), position);
return holder.getConvertView();
}
public abstract void convert(ViewHolder holder, T t, int pos);
}
所需要的ViewHolder
package com.roy.widgetdemo;
import android.content.Context;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
public class ViewHolder {
private SparseArray<View> mViews;
private int mPosition;
private View mConvertView;
public ViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
this.mPosition = position;
this.mViews = new SparseArray<View>();
this.mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
mConvertView.setTag(this);
}
public static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position){
if (convertView == null) {
return new ViewHolder(context, parent, layoutId, position);
}else {
ViewHolder holder = (ViewHolder) convertView.getTag();
holder.mPosition = position;
return holder;
}
}
/**
* 通過viewId獲取控件
* @param viewId
* @return
*/
@SuppressWarnings("unchecked")
public <T extends View> T getView(int viewId){
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
Log.d("debug", "(T) view =" + (T) view);
return (T) view;
}
public View getConvertView() {
return mConvertView;
}
/**
* 設置TextView的值
* @param viewId
* @param text
* @return
*
* 可以根據項目item需要控件的需求添加設置各種控件的方法
*/
public ViewHolder setText(int viewId, String text){
TextView tv = getView(viewId);
tv.setText(text);
return this;
}
/**
* 設置本地圖片資源
* @param viewId
* @param resId
* @return
*/
public ViewHolder setImageResource(int viewId, int resId){
ImageView iv = getView(viewId);
iv.setImageResource(resId);
return this;
}
/**
* 設置CheckBox的選擇狀態
* @param viewId
* @param resId
* @return
*/
public ViewHolder setCheckd(int viewId, Boolean b){
CheckBox cb = getView(viewId);
cb.setChecked(b);
return this;
}
}
寫的item佈局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="3dp"
>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="adsfsdfsdfsdfsdfsdf"
android:background="@drawable/rec"
android:padding="5dp"
/>
</android.support.constraint.ConstraintLayout>
測試佈局
<?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.roy.widgetdemo.CustomLayout
android:id="@+id/custom"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:horizontal_spacing="10dp"
>
</com.roy.widgetdemo.CustomLayout>
</android.support.constraint.ConstraintLayout>
來測試以下代碼
package com.roy.widgetdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CustomLayout customLayout = findViewById(R.id.custom);
List<String> list = new ArrayList<>();
for (int i = 0; i <5; i++) {
list.add("你好啊美女");
list.add("美女有空嗎約嗎");
list.add("你幾個啊");
list.add("不想");
list.add("你爲啥呢");
}
CommonAdapter<String> commonAdapter = new CommonAdapter<String>(this,list,R.layout.item) {
@Override
public void convert(ViewHolder holder, String s, int pos) {
TextView tv = holder.getView(R.id.tv);
tv.setText(s);
}
};
customLayout.setAdapter(commonAdapter);
}
}
效果:
demo:下載地址