1. 功能描述
目前只能支持三張圖片认轨,支持橫豎屏模式绅络,手指滑動翻頁到下一張卡片,手指點擊也可以切換到當前卡片嘁字,并且選中的卡片會在整個ViewGroup的最上層恩急,會被放大,可以自定義放大動畫的時長纪蜒。最基本的Android自定義控件衷恭,大神就別看了。
來先看效果圖吧:
支持豎屏模式
gif
也支持橫屏模式:
gif2
屬性 | 描述 | 默認值 |
---|---|---|
scc_anim_duration | 卡片放大動畫時間 | 300 |
scc_edge | 每個卡片頂邊和底邊的距離 | 60 |
scc_type | 豎屏還是橫屏模式 | VERTICAL |
scc_min_change_distance | 手指最小滑動距離才會翻頁 | 20 |
主要是想熟悉一下自定義控件的基本測量和布局方式纯续,其實使用LinearLayout或者是FrameLayout來做會更加方便随珠,但是這個時候就不需要我們自己去重寫onMeasure和onLayout方法了。
支持的自定義屬性:
屬性 | 描述 | 默認值 |
---|---|---|
scc_anim_duration | 卡片放大動畫時間 | 300 |
scc_edge | 每個卡片頂邊和底邊的距離 | 60 |
scc_type | 豎屏還是橫屏模式 | VERTICAL |
scc_min_change_distance | 手指最小滑動距離才會翻頁 | 20 |
2. 實現(xiàn)原理
把ViewGroup中的三個View(可為任意的三個控件)按照預設好的邊距和padding測量大小杆烁,然后三個view根據(jù)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;
// 循環(huán)測量每一個View
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 系統(tǒng)自動測量子View:
// measureChild(childView, widthMeasureSpec, heightMeasureSpec);
/** 以一個精確值來測量子View的寬度 */
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
}
c. 測量子View位置
位置確定最基本原理:
onlayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
// 循環(huán)測量每一個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. 手勢交互邏輯
在手指滑動的時候為了防止頻繁觸發(fā)翻頁,我使用了handler去發(fā)送翻頁消息吊奢。
/**
* 事件分發(fā)
* 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;
}
4. xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<danxx.library.widget.StackCardContainer xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/threeDViewContainer"
app:scc_anim_duration="300"
app:scc_edge="90"
app:scc_padding="70"
app:scc_type="horizontal"
app:scc_min_change_distance="20"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="420dp">
<android.support.v7.widget.CardView
android:id="@+id/view1"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/card_view_bg0"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:text="血戰(zhàn)鋼鋸嶺"
android:padding="6dp"
android:textSize="22sp"
android:lines="1"
android:gravity="center"
android:layout_gravity="bottom"
android:background="#CAC26F"/>
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView
android:id="@+id/view2"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/card_view_bg1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:text="你的名字"
android:padding="6dp"
android:textSize="22sp"
android:gravity="center"
android:lines="1"
android:layout_gravity="bottom"
android:background="#0085BA"/>
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView
android:id="@+id/view3"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/card_view_bg2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:text="從你的全世界路過"
android:lines="1"
android:padding="6dp"
android:textSize="22sp"
android:gravity="center"
android:layout_gravity="bottom"
android:background="#4EC9AD"/>
</android.support.v7.widget.CardView>
</danxx.library.widget.StackCardContainer>
其實就是在我們自定義的StackCardContainer
容器中放置了三個CardView
隧熙,至于點擊事件和數(shù)據(jù)綁定等完全由用戶自己去設置和綁定。StackCardContainer
自定義控件只是改變了子View的布局方式并處理手勢交互罷了幻林。