admin 管理员组

文章数量: 887006

android单个容器设成横向,Android自定义View

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. 实现原理

把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位置

位置确定最基本原理:

onlayout

@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;

}

4. xml布局文件

xmlns:app=""

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:id="@+id/view1"

app:cardCornerRadius="10dp"

app:cardElevation="10dp"

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:scaleType="centerCrop"

android:src="@mipmap/card_view_bg0"/>

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:textColor="@android:color/white"

android:text="血战钢锯岭"

android:padding="6dp"

android:textSize="22sp"

android:lines="1"

android:gravity="center"

android:layout_gravity="bottom"

android:background="#CAC26F"/>

android:id="@+id/view2"

app:cardCornerRadius="10dp"

app:cardElevation="10dp"

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:scaleType="centerCrop"

android:src="@mipmap/card_view_bg1"/>

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:id="@+id/view3"

app:cardCornerRadius="10dp"

app:cardElevation="10dp"

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:scaleType="centerCrop"

android:src="@mipmap/card_view_bg2"/>

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"/>

其实就是在我们自定义的StackCardContainer容器中放置了三个CardView,至于点击事件和数据绑定等完全由用户自己去设置和绑定。StackCardContainer自定义控件只是改变了子View的布局方式并处理手势交互罢了。

本文标签: android单个容器设成横向 Android自定义View