您的位置:首页 > 其它

自定义ViewGroup实现HorizontalScrollView的效果

2018-01-21 10:48 441 查看
自定义ViewGroup主要有三个步骤:

1. 实现onLayout进行布局

2. 实现onMeasure测量

3. 支持margin

4. 实现弹性滑动

5. 解决滑动冲突

class CustomViewByViewGroup(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int):
ViewGroup(context, attrs, defStyleAttr, defStyleRes){

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int):this(context, attrs, defStyleAttr, 0)
constructor(context: Context, attrs: AttributeSet):this(context, attrs, 0, 0)
constructor(context: Context): this(context, null, 0, 0)

var lastX: Float = 0f
var lastY: Float = 0f

/**===========================================================
* 1. 继承ViewGroup必须实现onLayout方法
*============================================================*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
var childLeft = paddingLeft //需要处理padding
for(i in 0 until childCount){
val childView = getChildAt(i)
if(childView.visibility != View.GONE){
val childWidth = childView.measuredWidth

//2. 额外处理margin属性
val childLayoutParams = childView.layoutParams as MarginLayoutParams
childLeft += childLayoutParams.leftMargin
childView.layout(childLeft,
childLayoutParams.topMargin + paddingTop,
childLeft + childWidth,
childLayoutParams.topMargin  + paddingTop + childView.measuredHeight) //一定要根据margin处理好四个顶点坐标
childLeft += childWidth + childLayoutParams.rightMargin
}
}
}

/**=====================================================================
* 2. 定义ViewGroup的布局测量过程(也需要额外处理margin)
*=======================================================================*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)

var measureWidth = 0
var measureHeight = 0

//2. 需要测量所有子View!
measureChildren(widthMeasureSpec, heightMeasureSpec)

//3. 本身宽高的模式均为wrap_content, 需要根据子View来获得
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
for(i in 0 until childCount){
val childView = getChildAt(i)
measureWidth += childView.measuredWidth //测量出总宽度

//6. 处理marigin
val childLayoutParams = childView.layoutParams as MarginLayoutParams
measureWidth += childLayoutParams.leftMargin + childLayoutParams.rightMargin

val totalCurChildHeight = childView.measuredHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin
if(totalCurChildHeight > measureHeight){
measureHeight = totalCurChildHeight //选取子View中高度最大的
}
}
//7. 处理padding
measureWidth += paddingLeft + paddingRight
measureHeight += paddingTop + paddingBottom
setMeasuredDimension(measureWidth, measureHeight)
}
//4. 仅有高度是wrap_content
else if(heightSpecMode == MeasureSpec.AT_MOST){
//获取所有子View最大的高度,宽度直接用给定的尺寸
for(i in 0 until childCount){
val childView = getChildAt(i)

// 处理高度(wrap_content)上marigin
val childLayoutParams = childView.layoutParams as MarginLayoutParams

val totalCurChildHeight = childView.measuredHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin
if(totalCurChildHeight > measureHeight){
measureHeight = totalCurChildHeight //选取子View中高度最大的
}
}
measureHeight += paddingTop + paddingBottom //处理高度的padding
setMeasuredDimension(widthSpecSize, measureHeight)
}
//5. 仅有宽度是wrap_content
else if(widthSpecMode == MeasureSpec.AT_MOST){
for(i in 0 until childCount){
val childView = getChildAt(i)
measureWidth += childView.measuredWidth

//  处理宽度(wrap_content)上marigin
val childLayoutParams = childView.layoutParams as MarginLayoutParams
measureWidth += childLayoutParams.leftMargin + childLayoutParams.rightMargin
}
measureWidth += paddingLeft + paddingRight            //  处理宽度的padding
setMeasuredDimension(measureWidth, heightSpecSize)//高度直接用给定的尺寸
}
}

/**===============================================================
* 1. 要支持Margin功能,必须要重写方法,并实现自己LayoutParams
*=================================================================*/
override fun generateDefaultLayoutParams() = MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
override fun generateLayoutParams(attrs: AttributeSet) = MyLayoutParams(context, attrs)
override fun generateLayoutParams(p: LayoutParams): MyLayoutParams{
when(p){
is LayoutParams -> return MyLayoutParams(p)
is MarginLayoutParams ->  return MyLayoutParams(p)
else -> return MyLayoutParams(p)
}
}

open class MyLayoutParams : MarginLayoutParams {
constructor(c: Context, attrs: AttributeSet) : super(c, attrs)
constructor(width: Int, height: Int) : super(width, height)
constructor(p: ViewGroup.LayoutParams) : super(p) {}
constructor(source: ViewGroup.MarginLayoutParams) : super(source)
}

/**==============================================================
* 3. 滑动所需的scroller
*==============================================================*/

val mScroller: Scroller
init {
mScroller = Scroller(context)
}
override fun computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.currX, mScroller.currY)
invalidate()
}
}
fun smoothScrollTo(destX: Int, destY: Int){
mScroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY, 500)
invalidate()
}
var curChildIndex = 0

/**==============================================================
* 4. 左右滑动+页面切换(类似HorizontalScrollView)
*==============================================================*/
override fun onTouchEvent(event: MotionEvent): Boolean {
val curX = event.x
val curY = event.y

when(event.action){
//1. View跟随手指滑动
MotionEvent.ACTION_MOVE -> {
val deltaX = curX - lastX
scrollBy(-deltaX.toInt(), 0)
}
//2. 滑动一定距离后进行页面弹性切换
MotionEvent.ACTION_UP -> {
//3. 滑动的距离需要为 scrollx - 当前View左边所有View的总宽度 = distance
var totalLeftChildrenWidth = 0
for(i in 0 until curChildIndex){
// 计算出加上子View测量宽+左右margin=总child长度
val childView = getChildAt(i)
val childLayoutParams = childView.layoutParams as MarginLayoutParams
totalLeftChildrenWidth += childView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
}
val distance = scrollX - totalLeftChildrenWidth

//4. 比较距离和当前View的宽度,来判断是否切换页面
val curChildView = getChildAt(curChildIndex)
val childLayoutParams = curChildView.layoutParams as MarginLayoutParams
val curChildWidth = curChildView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
if(Math.abs(distance) > curChildWidth / 3){
if(distance < 0 && curChildIndex > 0){ //向左滑动(最左面页面不会向左滑动)
curChildIndex--
}else if(distance > 0 && (curChildIndex < childCount - 1)){ //向右滑动
curChildIndex++
}
}

//5. 滑动到当前Index表示的页面(Scroller弹性滑动)
var totalLeftChildWidth = 0
for(i in 0 until curChildIndex){
// 计算除了当前View的左侧所有View的总宽度
val childView = getChildAt(i)
val childLayoutParams = childView.layoutParams as MarginLayoutParams
totalLeftChildWidth += childView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
}
smoothScrollTo(totalLeftChildWidth,0)
}
else -> {
}
}
lastX = curX
lastY = curY
return true
}

/**==============================================================
* 5. 处理滑动冲突(水平方向的就直接拦截,竖直方向的给子View)
*==============================================================*/
var downX = 0f
var downY = 0f
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
var isIntercept = super.onInterceptTouchEvent(ev)
when(ev.action){
MotionEvent.ACTION_DOWN ->{
downX = ev.x
downY = ev.y
isIntercept = false
}
MotionEvent.ACTION_MOVE ->{
val deltaX = ev.x - downX
val deltaY = ev.y - downY
isIntercept = (Math.abs(deltaX) >= Math.abs(deltaY)) //deltaX大,表示左右滑动,直接返回true
}
MotionEvent.ACTION_UP ->{
isIntercept = false
}
}
/**===========================================
* 注意:
* 1. 页面切换后,lastX/Y还是切换时刻的值,
* 2. 拦截后直接跳到onTouchEvent的ACTION_MOVE执行
* 3. 错误的lastX会导致滑动错乱,因此进入MOVE处理前,这里要记录最新的lastX/Y
*=============================================*/
lastX = ev.x
lastY = ev.y
return isIntercept
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐