自定義控件的分類
[1]通過系統(tǒng)提供的原生控件進(jìn)行組合 來達(dá)到自定義的需求
[2]定義一個(gè)類繼承View(繼承原生的類)
定義一個(gè)類繼承ViewGroup (五大布局都繼承自viewgroup)
下拉列表(原生控件進(jìn)行組合)
功能分析:
由edittext 按鈕 popupwindow listview 通過這樣幾個(gè)控件進(jìn)行組合達(dá)到需求
實(shí)現(xiàn)步驟
**1先畫布局定義布局 **
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
tools:context=".MainActivity" >
<EditText
android:id="@+id/et_number"
android:layout_width="250dp"
android:layout_height="wrap_content" />
<!--去掉背景-->
<!--android:background="@null"-->
<ImageButton
android:id="@+id/ib_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@id/et_number"
android:background="@null"
android:src="@drawable/down_arrow" />
</RelativeLayout>
2 當(dāng)點(diǎn)擊按鈕的時(shí)候彈出popupwindow
//彈出popupwindow 寬高 和Edittext寬高一樣
protected void showPopUpWindow() {
//[0]準(zhǔn)備popupwindow 要展示的數(shù)據(jù)(listview)
ListView contentView = initListView();
//[1]構(gòu)造popupwindow
Popupwindow沒必要new多次, 因此先判斷一下popupwindow是否為空
if (popupWindow == null) {
popupWindow = new PopupWindow(contentView, et_number.getWidth() - 8, 250, true);
popupwindow默認(rèn)不讓獲取焦點(diǎn)(因此使用構(gòu)造方法為4個(gè)參數(shù)的)
//參1:要展示的view 參2: 寬 參3: 高 參4:Boolean—popupwindow是否可以獲取焦點(diǎn)
//設(shè)置背景
//點(diǎn)擊popupwindow外面popupwindow不消失,想讓popupwindow消失,因此給popupwindow設(shè)置背景
popupWindow.setBackgroundDrawable(new ColorDrawable());
}
//[2]展示popupwindow anchor:
//參1popupwindow依賴哪個(gè)控件(展示在哪個(gè)控件下方)
// 參2 偏移量x
//參3 偏移量y
//偏移量可以讓盡量popupwindow與依賴的控件對(duì)齊的一致
popupWindow.showAsDropDown(et_number, 4, -4);
}
再設(shè)置adapter--點(diǎn)擊事件
繪制實(shí)戰(zhàn)--重寫ondraw方法中—CANVAS.DRAW畫
//將new paint實(shí)例放在構(gòu)造方法中
//在ondraw方法中避免申請(qǐng)paint對(duì)象,因?yàn)橛锌赡茉诋嫷倪^程中申請(qǐng)多次,最好寫在構(gòu)造方法中
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//[1]創(chuàng)建畫筆類
mPaint = new Paint();
}
//[1]畫線 兩點(diǎn)確定一條線
canvas.drawLine(0,0,100,100,paint);
//參1 參2 起點(diǎn)坐標(biāo)
//參3 參4 終點(diǎn)坐標(biāo)
//參5 畫筆
//[2]畫圓 需要知道圓心和半徑
//畫圓 知道圓心 和 半徑 通過cx cy確定圓心 radius:半徑
canvas.drawCircle(100,100,30,mPaint);
//參1 參2 確定圓心坐標(biāo)(在view上畫圓,因此找圓心只要是view寬高的一半即可)
//參3 半徑
//參4 paint
//修改畫筆的(屬性)
//修改畫筆顏色(畫筆默認(rèn)的顏色是黑色的)
mPaint.setColor(Color.RED);
//設(shè)置畫筆樣式 空心(默認(rèn)實(shí)心)
mPaint.setStyle(Style.STROKE);
//去除鋸齒
mPaint.setAntiAlias(true);
// [3]畫圖片
//將圖片轉(zhuǎn)換成bitmap
// 2把haha.jpg轉(zhuǎn)換成一個(gè)bitmap對(duì)象
mBitmap=BitmapFactory.decodeResource(
getResources(),R
.drawable.haha);
//畫(bitmap)
canvas.drawBitmap(mBitmap,10,10,mPaint);
//參1 bitmap
//參2 距view 左邊的位置
//參3 距view 頂部的位置
//參4 paint
//[4]畫三角形(畫路徑)
//定義三個(gè)點(diǎn) 畫一個(gè)三角形
int x1 = 100, y1 = 0;
int x2 = 20, y2 = 180;
int x3 = 180, y3 = 180;
//拿到path路徑對(duì)象--先移動(dòng)到第一個(gè)點(diǎn)--然后挨個(gè)連接--有頭有尾(其實(shí)就是構(gòu)造出三角形的路徑)
mPath=new
Path();
mPath.moveTo(x1,y1); //先移動(dòng)到x1 y1點(diǎn)
mPath.lineTo(x3,y3); //連接
mPath.lineTo(x2,y2);
mPath.lineTo(x1,y1);
//將構(gòu)造的路徑畫出來(畫三角形)
//把三個(gè)點(diǎn)連起來即為三角形
canvas.drawPath(mPath,mPaint);
//參1 path 參2 paint
// [5]畫扇形 畫扇形drawArc
//rectF控制畫扇形的范圍
canvas.drawArc(rectF,0,swapAngle,false,mPaint);
//參1 矩形區(qū)域(通過這個(gè)矩形來限定畫的扇形的大小)
//參2 開始的一個(gè)角度,與水平向右的夾角(順時(shí)針為正)
//參3 掃過的的一個(gè)角度,與水平向右的夾角(順時(shí)針為正)
//參4 bollean(true 圓弧有邊 false 圓弧沒有邊)
//參5 paint
//構(gòu)造矩形
rectF=new
RectF(5,5,170,170);
//[6]動(dòng)態(tài)畫圓
//通過畫扇形drawArc方法來畫圓(調(diào)用一次方法只能畫一段圓弧)
//我們只需模擬一些數(shù)據(jù),動(dòng)態(tài)的改變參3的值即可
//因此需要定義一個(gè)變量,接受傳來的值
//將傳來的值傳給canvas.drawArc方法的參3(這個(gè)方法寫在ondraw中)
//請(qǐng)求重新繪制
//設(shè)置點(diǎn)擊事件開始—模擬數(shù)據(jù)畫圓
// 點(diǎn)擊按鈕 動(dòng)態(tài)畫圓
public void click(View v) {
// 開啟一個(gè)子線程
new Thread() {
public void run() {
// 模擬數(shù)據(jù)
for (int i = 1; i <= 100; i++) {
// 開始畫
myView.startDraw(i);
// 睡眠50毫秒(因?yàn)楫嫷奶?開不出效果,睡一會(huì)兒)(睡眠開子線程)
SystemClock.sleep(50);
}
}
;
}.start();
}
//找到所要畫圓的view,創(chuàng)建方法,傳遞數(shù)據(jù), 并請(qǐng)求重新繪制
public void startDraw(int x) {
//定義一個(gè)變量(成員變量)進(jìn)行傳值
mprogress = x;
//請(qǐng)求重新繪制
// 請(qǐng)求重新繪制 --->onDraw方法就會(huì)執(zhí)行
// invalidate();
// 如果不是ui 線程 應(yīng)該調(diào)用下面這個(gè)方法()--請(qǐng)求重新繪制 --->onDraw方法就會(huì)執(zhí)行
postInvalidate();
}
//重寫ondraw方法,畫圓弧,
//往當(dāng)前控件上畫內(nèi)容
@Override
protected void onDraw(Canvas canvas) {
// rectF控制畫扇形的范圍
數(shù)據(jù)傳入?yún)?shù)3
float swapAngle = mprogress / 100f * 360;
canvas.drawArc(rectF, 0, swapAngle, false, mPaint);
// 參1 矩形區(qū)域(通過這個(gè)矩形來限定畫的扇形的大小)
// 參2 開始的一個(gè)角度,與水平向右的夾角(順時(shí)針為正)
// 參3 掃過的的一個(gè)角度,與水平向右的夾角(順時(shí)針為正)
// 參4 bollean(true 圓弧有邊 false 圓弧沒有邊)
// 參5 paint
}
//構(gòu)造矩形
rectF=new RectF(5,5,170,170);
可滑動(dòng)開關(guān) –繼承view(自定義控件)
需求分析 :實(shí)際是由2張圖片 疊加到一起組成一個(gè)View
自己做自定義控件就兩種方式,繼承view還是繼承viewgroup---如果需要包裹孩子就繼承viewgroup
定義一個(gè)類繼承view---添加兩個(gè)參數(shù)的構(gòu)造方法
public class ToogleView extends View {
public ToogleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
使用該view--在xml中聲明
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:itheima="http://schemas.android.com/apk/res/com.itheima.toogleview"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.itheima.toogleview.ToogleView
android:id="@+id/toogleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
itheima:toogleState="false" />
</RelativeLayout>
測量-根據(jù)自己的需求對(duì)當(dāng)前控件進(jìn)行測量—調(diào)用onmeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自己測量控件的寬和高 和當(dāng)前背景圖片一樣寬 一樣高
setMeasuredDimension(toogleBgBitmap.getWidth(), toogleBgBitmap.getHeight());
}
///把圖片變成bitmap---為了獲取背景圖片的寬高
// [1]找到背景圖片和滑動(dòng)塊圖片 變成bitmap
toogleBgBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_background);
toogleSlideBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_slidebg);
不用排版,父類已經(jīng)排好版
控件上繪制內(nèi)容—重寫ondraw—拿掉super—畫好之后控件就展示上來了
// 往當(dāng)前控件上畫內(nèi)容 要重寫onDraw方法
@Override
protected void onDraw(Canvas canvas) {
//畫開關(guān)背景--畫圖片的時(shí)候不用paint
canvas.drawBitmap(toogleBgBitmap, 0, 0, null);
//畫滑動(dòng)塊--畫圖片的時(shí)候不用paint
canvas.drawBitmap(toogleSlideBitmap, slideLeftPosition, 0, null);
}
讓當(dāng)前的view處理事件—畫好開關(guān)后——需要重寫onTouchEvent—有按下 移動(dòng) 抬起 等事件—返回true(當(dāng)前view消費(fèi)事件)
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN://按下
break;
case MotionEvent.ACTION_MOVE://移動(dòng)
break;
case MotionEvent.ACTION_UP: // 手指抬起
break;
}
return true; // 代表讓當(dāng)前控件處理事件 消費(fèi)事件
}
按鈕移動(dòng)效果
獲取按下坐標(biāo)—獲取移動(dòng)后的坐標(biāo)—算出移動(dòng)距離—讓滑塊移動(dòng)這么長的距離(移動(dòng)著么長距離其實(shí)就是重新再新的坐標(biāo)上繪制按鈕)
//1獲取手指按下的x坐標(biāo)
downX=event.getX();
//1獲取手指移動(dòng)后坐標(biāo)
float moveX = event.getX();
//2 算出移動(dòng)距離
float distanceX = moveX - downX;
//指定滑塊移動(dòng)到的位置---賦值給繪制滑塊的參數(shù)3()
slideLeftPosition+=distanceX;
//更改起始點(diǎn)
// [4]改變一下起始點(diǎn)
downX=moveX;
//請(qǐng)求重新繪制 onDraw方法就會(huì)執(zhí)行
invalidate();
限定滑塊的左右邊界
//右邊界最大值—背景寬-滑塊的寬
slideLeftMax = toogleBgBitmap.getWidth() - toogleSlideBitmap.getWidth();
//對(duì)邊界進(jìn)行判斷
if (slideLeftPosition <= 0) {
slideLeftPosition = 0;
} else if (slideLeftPosition >= slideLeftMax) {
slideLeftPosition = slideLeftMax;
}
當(dāng)手指抬起時(shí),滑倒一個(gè)位置,抬起后接著滑動(dòng)到左邊或者右邊
//當(dāng)手指抬起時(shí),獲取到手指在的 位置,判斷滑塊的中心店位置,過去
//1算出背景中心點(diǎn)位置
float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
//2 算出滑動(dòng)塊的中心點(diǎn)位置 = slideLeftPosition + 滑塊背景的一半
float sliecenterPosition = slideLeftPosition
+ toogleSlideBitmap.getWidth() / 2;
//判斷滑塊與背景的相對(duì)位置
if (sliecenterPosition <= tooglebgCenterPosition) {
// 開關(guān)處于關(guān)
slideLeftPosition = 0;
} else {
slideLeftPosition = slideLeftMax;
}
View狀態(tài)發(fā)生改變時(shí)的回調(diào)事件--實(shí)現(xiàn)開關(guān)事件對(duì)應(yīng)的回調(diào)方法*
//我們想要在開關(guān)開啟或者關(guān)閉的時(shí)候,執(zhí)行一些邏輯要暴露此方法
//回調(diào)其實(shí)就是多態(tài)的一個(gè)應(yīng)用定義接口—暴回調(diào)方法
// 定義接口
public interface OnToogleViewListener {
// 當(dāng)開關(guān)的狀態(tài)發(fā)生改變的時(shí)候調(diào)用
void onToogleState(boolean state);
}
//開關(guān)狀態(tài)發(fā)生變化時(shí)調(diào)用該方法
//當(dāng)手指抬起且開關(guān)狀態(tài)發(fā)生變化—調(diào)用該方法判斷手指是否抬起器開關(guān)狀態(tài)是否改變定義變量判斷是否抬手
/**
* 是否抬起手 默認(rèn)false
*/
private boolean isHandup;
//當(dāng)手指抬起的時(shí)候置為true
isHandup = true;
//當(dāng)我們判斷為抬起之后將其設(shè)置為false
isHandup = false;
//定義開關(guān)默認(rèn)狀態(tài)
/**
* 代表開關(guān)的默認(rèn)狀態(tài)
*/
private boolean isOpen;
//當(dāng)手指抬起獲取滑動(dòng)后的開關(guān)狀態(tài)
//3回調(diào)我們定義接口方法
if (isHandup){
isHandup = false;
//4獲取滑動(dòng)后的一個(gè)狀態(tài)
boolean isOpenTemp = slideLeftPosition>0;
//判斷如果臨時(shí)狀態(tài)與默認(rèn)狀態(tài)不一致則說明開關(guān)狀態(tài)發(fā)生變化了
if (isOpen!=isOpenTemp && mListener!=null) {
//說明開關(guān)的狀態(tài)改變了 我們就要觸發(fā)我們定義的回調(diào)方法
mListener.onToogleState(isOpenTemp);
//發(fā)生改變后將發(fā)生改變后的狀態(tài)—賦值給默認(rèn)狀態(tài)
isOpen = isOpenTemp;
}
}
//當(dāng)開關(guān)狀態(tài)放生變化時(shí),觸發(fā)回調(diào)方法(返回相應(yīng)的值)
mListener.onToogleState(isOpenTemp);
//發(fā)生改變后將發(fā)生改變后的狀態(tài)—賦值給默認(rèn)狀態(tài)
isOpen = isOpenTemp;
//給view設(shè)置監(jiān)聽—傳入接口類型(子類)—給接口類型成員變量賦值
/**
* 設(shè)置開關(guān)的監(jiān)聽器
*/
public void setOnToogleViewListener(OnToogleViewListener l) {
this.mListener = l;
}
//使用view的監(jiān)聽事件-- ,當(dāng)監(jiān)聽到改變后執(zhí)行觸發(fā)后的方法 toogleView.setOnToogleViewListener(new OnToogleViewListener() {
//這個(gè)什么時(shí)候被觸發(fā)呢?
@Override
public void onToogleState(boolean state) {
if (state) {
Toast.makeText(getApplicationContext(), "開", 1).show();
}else {
Toast.makeText(getApplicationContext(), "關(guān)", 1).show();
}
}
});
設(shè)置點(diǎn)擊事件—也包含在狀態(tài)改變中
當(dāng)按下時(shí),獲取按下的時(shí)間
long startTime = System.currentTimeMillis();
當(dāng)手指抬起時(shí)記住抬起的時(shí)間
long endTime = System.currentTimeMillis();
判斷
時(shí)間間隔小于200ms為點(diǎn)擊事件
手指抬起的位置大于滑塊的右邊界
Else手指抬起的位置大于滑塊的左邊界
Else(大于200ms為滑動(dòng)事件)
case MotionEvent.ACTION_UP: // 手指抬起
int endTime = (int) (System.currentTimeMillis() - startTime);
System.out.println("endTiem:" + endTime);
if (endTime < 200 && event.getX() > toogleSlideBitmap.getWidth()) {
// 說明是點(diǎn)擊事件
slideLeftPosition = slideLeftMax;
} else if (endTime < 200) {
slideLeftPosition = 0;
} else {
// 1算出背景中心點(diǎn)位置
float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
// 2 算出滑動(dòng)塊的中心點(diǎn)位置 = slideLeftPosition + 滑塊背景的一半
float sliecenterPosition = slideLeftPosition
+ toogleSlideBitmap.getWidth() / 2;
if (sliecenterPosition <= tooglebgCenterPosition) {
// 開關(guān)處于關(guān)
slideLeftPosition = 0;
} else {
slideLeftPosition = slideLeftMax;
}
}
isHandup = true;
break;
}
給view設(shè)置自定義屬性
在res/values下創(chuàng)建attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="toogleView">
<attr name="toogleState" format="boolean" />
</declare-styleable>
</resources>
在layout布局中聲明
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kailing="http://schemas.android.com/apk/res/com.kailing.toogleview"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.kailing.toogleview.ToogleView
android:id="@+id/toogleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
kailing:toogleState="false" />
</RelativeLayout>
在構(gòu)造方法中獲取屬性值
// [2]通過AttributeSet獲取屬性值
String namespace = "http://schemas.android.com/apk/res/com.kailing.toogleview";
//參1 根據(jù)命名空間取值,參2屬性名,參3 缺省值(默認(rèn)值)
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);
//使用—屬性
// [3]調(diào)用setToogleState
setToogleState(toogleState);
//[7]view自定義屬性
//1)在values下創(chuàng)建一個(gè)attrs.xml 文件
//2)xml里面的內(nèi)容抄襲系統(tǒng)定義好的
//3)在布局中使用 自己定義一個(gè)命名空間
//4)在當(dāng)前的view的構(gòu)造方法里同AttributeSet獲取我們定義的值
ViewGroup繪制流程
定義一個(gè)類繼承viewgroup(會(huì)自動(dòng)重寫onLayout方法—就在這個(gè)方法里完成排版)
//定義一個(gè)類繼承viewgroup(會(huì)自動(dòng)重寫onLayout方法—就在這個(gè)方法里完成排版)
public class MyViewGroup extends ViewGroup {
//添加兩個(gè)參數(shù)的構(gòu)造方法
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
布局文件中使用(在這里自view還是看不見的)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.kailing.viewgroup.MyViewGroup
android:id="@+id/myViewGroup1"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
</com.kailing.viewgroup.MyViewGroup>
</RelativeLayout>
自己定義的viewgroup沒有對(duì)孩子進(jìn)行測量,和排版所以group里面的孩子顯示不出來
測量—onmeasure—測量孩子
當(dāng)前重寫onmeasure是對(duì)當(dāng)前自己的groupview進(jìn)行測量,讓父類進(jìn)行測量即可
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1獲取viewGroup的孩子
// 參1因?yàn)榫鸵粋€(gè)孩子--寫0即可,多個(gè)孩子for循環(huán)即可
// for (int i = 0; i < getChildCount(); i++) {}
View childAt = getChildAt(0);
//2//對(duì)孩子進(jìn)行測量—實(shí)際調(diào)用孩子的measure方法
//(參數(shù)寫0是個(gè)非法值,當(dāng)系統(tǒng)發(fā)現(xiàn)寫0會(huì)采用默認(rèn)的測量對(duì)當(dāng)前的孩子進(jìn)行測量)(默認(rèn)的測量:孩子在聲明的時(shí)候?qū)懙闹?-對(duì)應(yīng)的模式)
childAt.measure(0, 0);// 這行代碼執(zhí)行完--測量完了
//3 獲取孩子的寬度 高度(獲取測量之后 的寬度和高度)
measuredWidth = childAt.getMeasuredWidth();
measuredHeight = childAt.getMeasuredHeight();
// getMeasuredWidth 獲取到測量之后的值
// getWidth()當(dāng)view排版后才可以獲取
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
排版—完成對(duì)孩子進(jìn)行排版
// 要在這個(gè)方法里面完成對(duì)孩子進(jìn)行排版
// 參數(shù)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//找到孩子
View childAt = getChildAt(0);
//對(duì)孩子進(jìn)行排版
childAt.layout(l, t, measuredWidth, measuredHeight);
}
繪制—不需要重寫ondraw方法