1. 功能描述
目前只能支持三張圖片,支持橫豎屏模式,手指滑動翻頁到下一張卡片,手指點擊也可以切換到當前卡片,並且選中的卡片會在整個ViewGroup的最上層,會被放大,可以自定義放大動畫的時長。最基本的Android自定義控件,大神就別看了。
來先看效果圖吧:
支持豎屏模式
也支持橫屏模式:
主要是想熟悉一下自定義控件的基本測量和佈局方式,其實使用LinearLayout或者是FrameLayout來做會更加方便,但是這個時候就不需要我們自己去重寫onMeasure和onLayout方法了。
支持的自定義屬性:
屬性 | 描述 | 默認值 |
---|---|---|
scc_anim_duration | 卡片放大動畫時間 | 300 |
scc_edge | 每個卡片頂邊和底邊的距離 | 60 |
scc_type | 豎屏還是橫屏模式 | VERTICAL |
scc_min_change_distance | 手指最小滑動距離纔會翻頁 | 20 |
2. 實現原理
把ViewGroup中的三個View(可爲任意的三個控件)按照預設好的邊距和padding測量大小,然後三個view根據edge值來確定依次確定位置。我們沒有用到canvas、path或者paint。沒必要,我們只需要改變子View的繪製順序,檢測到用戶的滑動或者是點擊就invalidate重繪,把用戶選中的view放在最後繪製這樣就可以將當前選中的view放在最上層。這樣放大選中的view就不會被遮住。
3. 代碼講解
a. 改變子View的繪製次序
/**
* 獲取子控件dispatchDraw的次序,將當前選中的View放在最後繪製
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
//currentItemIndex 爲當前選中的View在ViewGroup中的position
if (currentItemIndex < 0) {
return i;
}
if (i < (childCount - 1)) {
if (currentItemIndex == i)
i = childCount - 1;
} else {
if (currentItemIndex < childCount)
i = currentItemIndex;
}
return i;
}
b. 測量子View大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 獲得此ViewGroup上級容器爲其推薦的寬和高,以及計算模式
*/
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
/**
* 先測量整個Viewgroup的大小
*/
setMeasuredDimension(sizeWidth, sizeHeight);
int childCount = getChildCount();
int childWidth, childHeight;
/**由於每一個子View的寬高都是一樣的所以就一起計算每一個View的寬高*/
if(ShapeType.VERTICAL.ordinal() == mShapeType){ //豎向模式
childWidth = getMeasuredWidth() - padding*2;
childHeight = getMeasuredHeight() - padding*2 - edge*2;
}else{ //橫向模式
childWidth = getMeasuredWidth() - padding*2 - edge*2;
childHeight = getMeasuredHeight() - padding*2;
}
int childWidthMeasureSpec = 0;
int childHeightMeasureSpec = 0;
// 循環測量每一個View
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 系統自動測量子View:
// measureChild(childView, widthMeasureSpec, heightMeasureSpec);
/** 以一個精確值來測量子View的寬度 */
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
}
c. 測量子View位置
位置確定最基本原理:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
// 循環測量每一個View
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//四個方向的margin值
int measureL = 0, measurelT = 0, measurelR = 0, measurelB = 0;
if(ShapeType.VERTICAL.ordinal() == mShapeType){ //豎向模式
switch (i){
case 0:
measureL = padding;
measurelT = padding;
measurelB = childView.getMeasuredHeight() + padding;
measurelR = childView.getMeasuredWidth() + padding;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
case 1:
measureL = padding;
measurelT = padding + edge;
measurelB = childView.getMeasuredHeight() + padding + edge;
measurelR = childView.getMeasuredWidth() + padding;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
case 2:
measureL = padding;
measurelT = padding + edge*2;
measurelB = childView.getMeasuredHeight() + padding + edge*2;
measurelR = childView.getMeasuredWidth() + padding;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
}
}else{ //橫向模式
switch (i){
case 0:
measureL = padding;
measurelT = padding;
measurelB = childView.getMeasuredHeight() + padding;
measurelR = childView.getMeasuredWidth() + padding;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
case 1:
measureL = padding + edge;
measurelT = padding;
measurelB = childView.getMeasuredHeight() + padding;
measurelR = childView.getMeasuredWidth() + padding + edge;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
case 2:
measureL = padding + edge*2;
measurelT = padding;
measurelB = childView.getMeasuredHeight() + padding;
measurelR = childView.getMeasuredWidth() + padding + edge*2;
childView.layout(measureL, measurelT, measurelR, measurelB);
break;
}
}
}
}
d. 手勢交互邏輯
在手指滑動的時候爲了防止頻繁觸發翻頁,我使用了handler去發送翻頁消息。
/**
* 事件分發
* onTouchEvent() 用於處理事件,返回值決定當前控件是否消費(consume)了這個事件
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("danxx", "onTouchEvent");
// return super.onTouchEvent(event);
/**以屏幕左上角爲座標原點計算的Y軸座標**/
int y;
if(ShapeType.VERTICAL.ordinal() == mShapeType){ //豎屏模式取Y軸座標
y = (int) event.getRawY();
}else{
y = (int) event.getRawX(); //橫屏模式取X軸座標
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "MotionEvent.ACTION_DOWN");
// 手指按下時記錄下y座標
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "MotionEvent.ACTION_MOVE");
// 手指向下滑動時 y座標 = 屏幕左上角爲座標原點計算的Y軸座標 - 手指滑動的Y軸座標
int m = y - lastY;
if(m>0 && m>changeDistance){ //手指向下滑動 或者是左滑
changeHandler.removeMessages(MSG_UP);
changeHandler.sendEmptyMessageDelayed(MSG_UP, animDuration);
}else if(m< 0&& Math.abs(m)>changeDistance){ //手指向上滑動 或者右滑
changeHandler.removeMessages(MSG_DOWN);
changeHandler.sendEmptyMessageDelayed(MSG_DOWN, animDuration);
}
// 記錄下此刻y座標
this.lastY = y;
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "MotionEvent.ACTION_UP");
break;
}
return true;
}
d. 上下或者左右翻頁代碼
/**
* 顯示下面的一頁
* 翻頁成功返回true,否則false
*/
private boolean downPage(){
if(1 == currentItemIndex){
FocusAnimUtils.animItem(getChildAt(currentItemIndex), false, 1.0f, animDuration);
// 重繪,改變堆疊順序
currentItemIndex = 2;
postInvalidate();
FocusAnimUtils.animItem(getChildAt(currentItemIndex), true, 1.06f, animDuration);
return true;
}else if(0 == currentItemIndex){
FocusAnimUtils.animItem(getChildAt(currentItemIndex), false, 1.0f, animDuration);
// 重繪,改變堆疊順序
currentItemIndex = 1;
postInvalidate();
FocusAnimUtils.animItem(getChildAt(currentItemIndex), true, 1.06f, animDuration);
return true;
}else if(2 == currentItemIndex){
return false;
}
return false;
}
/**
* 顯示上面的一頁
* 翻頁成功返回true,否則false
*/
private boolean upPage(){
if(1 == currentItemIndex){
FocusAnimUtils.animItem(getChildAt(currentItemIndex), false, 1.0f, animDuration);
// 重繪,改變堆疊順序
currentItemIndex = 0;
postInvalidate();
FocusAnimUtils.animItem(getChildAt(currentItemIndex), true, 1.06f, animDuration);
return true;
}else if(0 == currentItemIndex){
return false;
}else if(2 == currentItemIndex){
FocusAnimUtils.animItem(getChildAt(currentItemIndex), false, 1.0f, animDuration);
currentItemIndex = 1;
postInvalidate();
FocusAnimUtils.animItem(getChildAt(currentItemIndex), true, 1.06f, animDuration);
return true;
}
return false;
}