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
版权声明:本文标题:android单个容器设成横向,Android自定义View 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1732361854h1535442.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论