首先看一下效果图:
实现方法:
与下拉刷新的实现思路一样(只用把头布局的内容替换了),可以给ListView添加一个头布局,通过手势动态实时的控制头布局的paddingTop值,从而达到显示与隐藏的效果.
我在这里并没有使用ListView,而是使用Linearlayout,但是思路是完全一致的:
- 自定义view,继承自LinearLayout,设置布局为vertical
- 添加顶部布局,通过设置paddingTop控制显示与隐藏
- 添加ListView,触摸监听以及状态处理
- 圆点加载动画,手势动画(回弹效果)
步骤:
以下代码使用kotlin
最后有源码!!!
1.自定义view,继承自LinearLayout
设置布局为vertical,代码中设置或者xml中设置
class LoftView : LinearLayout {
constructor(context: Context) : super(context) {
LoftView(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
LoftView(context, null, -1)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.LoftView)
}
} TopLayout = LinearLayout(context)
TopLayout?.setClickable(true)
下图展示了界面显示的状态以及顶部view的paddingTop值的变化
2.添加头部以及滑动控件
- 当前view是继承自LinearLayout,并且布局方式为vertical,所以先后addView()即可
- 头部布局包括一个可以自由填充的view和小圆点,自由填充的view通过暴露方法有用户填充,小圆点通过自定义view绘制
TopLayout = LinearLayout(context)//头部的根布局
TopLayout?.setClickable(true)//设置可点击,否则可能子view无法响应触摸事件(后面会进一步说明)
TopLayout?.setBackgroundColor(background_top)//设置背景颜色
TopLayout?.orientation = LinearLayout.VERTICAL//设置布局为VERTICAL
公开添加头部和滑动view的方法
//添加自定义头部布局
fun createLoftView(context: Context?, resLayout: Int): LinearLayout? {
headlayout = View.inflate(context, resLayout, null) as LinearLayout?
headlayout?.visibility = View.INVISIBLE//默认隐藏,滑动动画开始后显示
return headlayout
}
/**
* 可滑动的view
*/
fun createRefreshView(refreshView: ListView) {
this.refreshView = refreshView
}
TopLayout?.addView(headlayout)
TopLayout?.addView(dotView, layoutParams)//添加小圆点
addView(TopLayout)//添加头部
addView(refreshView)//添加listview
3.测量头部布局的高度,设置paddingTop
- paddingTop=0时,TopView时正常显示的(state==open)
- paddingTop= -height,TopView为关闭状态(state==close)
TopLayout?.post({
subViewHeight = TopLayout?.height as Int
mPaddingTop = -subViewHeight
val paddingLeft = TopLayout?.paddingLeft as Int
val paddingRight = TopLayout?.paddingRight as Int
mpaddingBottom = TopLayout?.paddingTop as Int
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
})
// mPaddingTop = -subViewHeight所以此时的顶部view是隐藏的
4.手势拦截控制
- 顶部view有两种状态(open,close),手势滑动(向下滑动但view没有完全打开,向下滑动已经已经完全打开,向上滑动)
- 当view打开时,x轴的偏移量大于y轴的偏移量时,不拦截事件,交由子控件处理
- listview的第一可见的条目是第0条时,拦截触摸事件,由父控件(当前view)处理,拉出/关闭顶部view,否则不拦截,直到listview滑动到第一个item
- 当向上滑动, 顶部view为关闭状态时,不拦截事件
- 按下,抬起不拦截事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when ( ev?.action) {
MotionEvent.ACTION_DOWN ->//安下
{
mlastY = ev?.getY()
mlastX = ev?.getX()
mTouchEvent = false
}
MotionEvent.ACTION_MOVE ->//滑动
{
val flX = ev?.getX() - mlastX
val fl = ev?.getY() - mlastY
val abs = Math.abs(fl)
val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop//手指滑动阈值
if (abs > scaledTouchSlop) {
if (refreshView?.firstVisiblePosition == 0) {
mTouchEvent = true
} else {
mTouchEvent = false
}
if (fl < 0 && stateView == VIewState.CLOSE) {
mTouchEvent = false
}
if (stateView == VIewState.OPEN) {
if (Math.abs(flX) > abs) {
mTouchEvent = false
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> mTouchEvent = false//抬起
}
return mTouchEvent
}
5.手势控制
- 当在关闭状态向下滑动并且没有完全打开时
//scrollYValue是y轴偏移量为向下为正,增大
mPaddingTop = (decay_ratio * scrollYValue - subViewHeight).toInt()//decay_ratio * scrollYValue = subViewHeight时,view完全显示
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
- 完全显示依然向下,增加阻尼力度
mPaddingTop = (0.5 * decay_ratio * scrollYValue + 0.5 * (-subViewHeight)).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
- 按下之前是打开状态,向下滑动时
mPaddingTop = (0.5 * decay_ratio * scrollYValue).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)//PaddingTop,paddingBottom同时设置为mPaddingTop,使view在滑动时居中
- 打开状态向上滑动
mPaddingTop = (decay_ratio * scrollYValue).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
6.抬起以及动画控制
- 向下滑动超过高度的2/3时手指抬起:
moveAnimation(-mPaddingTop, mPaddingTop)//滚动打开view
stateView = VIewState.OPEN//状态设置为OPEN
dotHideAnim()//小圆点隐藏动画
- 向下滑动没有超过高度的2/3时手指抬起:
moveAnimation(-mPaddingTop, subViewHeight)//滚动关闭view
stateView = VIewState.CLOSE//状态设置为CLOSE
headlayout?.visibility = View.INVISIBLE//头部隐藏
dotView?.alpha = 1.0f//隐藏小圆点
- 完全打开后抬起:
moveAnimation(-mPaddingTop, mPaddingTop)//滚动到view原始大小位置
stateView = VIewState.OPEN//状态设置为OPEN
dotHideAnim()//隐藏小圆点
- 向上滑动后抬起:
moveAnimation(-mPaddingTop, subViewHeight)//滚动关闭view
headlayout?.visibility = View.INVISIBLE//头部隐藏
stateView = VIewState.CLOSE//状态设置为CLOSE
dotView?.alpha = 1.0f//隐藏小圆点
7.滚动过渡动画(回弹效果)
var mScroller: Scroller = Scroller(context, DecelerateInterpolator())//滑动器
/**
* view滚动回弹动画
*/
fun moveAnimation(startY: Int, y: Int) {
//前两个参数是起始位置,后两个参数分别表示x轴,y轴的偏移量,最后的参数是滚动时间
mScroller?.startScroll(0, startY, 0, y, 400);
invalidate()//刷新view会回调到computeScroll()方法,可以拿到一组偏移量的变化值
}
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
val currY = mScroller?.getCurrY()//实时获取y轴的偏移值
TopLayout?.setPadding(paddingLeft, -currY, paddingRight, mpaddingBottom)
}
invalidate()//刷新view
}
小圆点动画以及顶部view向下平移动画分别使用补间动画和属性动画,比较简单不用解释了
8.小圆点的绘制
自定义view,重点有注释,(小圆点的透明度的控制可以忽略)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = (width / 2).toFloat()
val centerY = (height / 2).toFloat()
//percent是view显示部分的百分比,完全打开时为1,关闭时为0
val fl = 255 * percent * 1.5f+30//控制透明度
mPaint.alpha = if(fl>255) 255 else fl.toInt()
if (percent <= 0.3f) {//小于1/3画一个圆圈
val radius = percent * 3.33f * maxRadius
canvas.drawCircle(centerX, centerY, radius, mPaint)
} else {//大于2/3画三个个圆
val afterPercent = (percent - 0.3f) / 0.7f
if (afterPercent<=1) {
val radius = maxRadius - maxRadius / 2f * afterPercent
Log.e("afterPercent--->", afterPercent.toString())
canvas.drawCircle(centerX, centerY, radius, mPaint)
canvas.drawCircle(centerX - afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
canvas.drawCircle(centerX + afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
}else if (afterPercent>1){//完全显示依然在向下滑动时,afterPercent会大于1
var d = afterPercent - 1.0
d= if (d>1) 1.0 else d
val fl =(1-d*2) * 255
mPaint.alpha = if(fl<60) 0 else fl.toInt()
canvas.drawCircle(centerX, centerY, maxRadius/2, mPaint)
canvas.drawCircle(centerX - maxDist, centerY, maxRadius / 2, mPaint)
canvas.drawCircle(centerX + maxDist, centerY, maxRadius / 2, mPaint)
}
}
}
源码
class LoftView : LinearLayout {
var headlayout: LinearLayout? = null//顶部view的子控件,由用户添加
var mScroller: Scroller = Scroller(context, DecelerateInterpolator())//滑动器
var mTouchEvent: Boolean = false//是否拦截点击事件
var scrollYValue = 0f//手指Y轴滑动的距离
var PLACE = 0//顶部view的显示位置(顶部或底部,)
var subViewHeight = 0//顶部view的height
var refreshView: ListView? = null
var decay_ratio = 0.5 //阻尼系数
var mpaddingBottom = 0//顶部view的paddingBottom
var mPaddingTop = 0//顶部view的PaddingTop
var mlastY = 0f//手指安下的y轴坐标值
var mlastX = 0f//手指安下的y轴坐标值
var stateView = VIewState.CLOSE//顶部view的显示状态,默认是关闭状态
var stateMove = TouchState_Move.NORMAL//手势滑动状态
var dotView: DotView? = null//小圆点
var TopLayout: LinearLayout? = null//顶部view的父控件
var background_top: Int = 0xFFe7e7e7.toInt() //默认颜色
constructor(context: Context) : super(context) {
LoftView(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
LoftView(context, null, -1)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.LoftView)
PLACE = obtainStyledAttributes.getInt(R.styleable.LoftView_place, 0)
background_top = obtainStyledAttributes.getColor(R.styleable.LoftView_background_top, 0xFFe7e7e7.toInt())
}
/**
* 可滑动的view
*/
fun createRefreshView(refreshView: ListView) {
this.refreshView = refreshView
}
/**
* 子view
*/
fun createLoftView(context: Context?, resLayout: Int): LinearLayout? {
headlayout = View.inflate(context, resLayout, null) as LinearLayout?
headlayout?.visibility = View.INVISIBLE
return headlayout
}
/**
* 构建顶部view布局
*/
fun buildView() {
when (refreshView) {
null -> refreshView = ListView(context)
}
when (headlayout) {
null -> throw LoftException("未初始化下拉view")
else -> {
dotView = DotView(context!!)
val layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 40)
TopLayout = LinearLayout(context)
TopLayout?.setClickable(true)
TopLayout?.setBackgroundColor(background_top)
TopLayout?.orientation = LinearLayout.VERTICAL
TopLayout?.post({
subViewHeight = TopLayout?.height as Int
mPaddingTop = -subViewHeight
val paddingLeft = TopLayout?.paddingLeft as Int
val paddingRight = TopLayout?.paddingRight as Int
mpaddingBottom = TopLayout?.paddingTop as Int
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
})
//暂不支持底部显示
when (PLACE) {
0, 1 -> {
TopLayout?.addView(headlayout)
TopLayout?.addView(dotView, layoutParams)
addView(TopLayout)
addView(refreshView)
}
//可以忽略
// 1 -> {
// this.addView(refreshView)
// TopLayout?.addView(expendPoint, layoutParams)
// TopLayout?.addView(headlayout)
// this.addView(TopLayout)
// }
}
}
}
}
/**
* 设置顶部背景颜色
*/
fun setBackgroundTop(color: Int) {
this.background_top = color
}
/**
* 处理触摸事件
*/
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when ( ev?.action) {
MotionEvent.ACTION_DOWN ->//安下
{
mlastY = ev?.getY()
mlastX = ev?.getX()
mTouchEvent = false
}
MotionEvent.ACTION_MOVE ->//滑动
{
val flX = ev?.getX() - mlastX
val fl = ev?.getY() - mlastY
val abs = Math.abs(fl)
val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop//手指滑动阈值
if (abs > scaledTouchSlop) {
if (refreshView?.firstVisiblePosition == 0) {
mTouchEvent = true
} else {
mTouchEvent = false
}
if (fl < 0 && stateView == VIewState.CLOSE) {
mTouchEvent = false
}
if (stateView == VIewState.OPEN) {
if (Math.abs(flX) > abs) {
mTouchEvent = false
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> mTouchEvent = false//抬起
}
return mTouchEvent
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
val action = ev?.action;
when (action) {
MotionEvent.ACTION_DOWN -> mTouchEvent = true//安下
MotionEvent.ACTION_MOVE ->//滑动
{
scrollYValue = (ev?.getY() - mlastY)
val abs = Math.abs(scrollYValue)
val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
if (abs > scaledTouchSlop) {
mTouchEvent = true
if (scrollYValue > 0) {
if (refreshView?.firstVisiblePosition == 0) {
if (stateView == VIewState.CLOSE) {
if (mPaddingTop < 0) {//向下滑动但是头部空间没完全显示
mPaddingTop = (decay_ratio * scrollYValue - subViewHeight).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
stateMove = TouchState_Move.DOWN_NO_OVER
dotView?.setPercent(1 - (mPaddingTop.toFloat() / (-subViewHeight)))
if (mPaddingTop > -subViewHeight / 2) {
showTodown(headlayout!!, 400)
}
} else if (mPaddingTop >= 0) {//头部空间没哇完全显示依然向下滑动
mPaddingTop = (0.5 * decay_ratio * scrollYValue + 0.5 * (-subViewHeight)).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
stateMove = TouchState_Move.DOWN_OVER
dotView?.setPercent(1 - (mPaddingTop.toFloat() / (-subViewHeight)))
}
} else {
mPaddingTop = (0.5 * decay_ratio * scrollYValue).toInt()
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
stateMove = TouchState_Move.DOWN_OVER
}
}
} else {
if (refreshView?.firstVisiblePosition == 0) {
if (stateView == VIewState.CLOSE) {
mPaddingTop = -subViewHeight
} else {
mPaddingTop = (decay_ratio * scrollYValue).toInt()
if (mPaddingTop <= -subViewHeight) {
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
mPaddingTop = -subViewHeight
stateView == VIewState.CLOSE
} else {
TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
stateMove = TouchState_Move.UP
}
}
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->//抬起
{
if (mPaddingTop > -subViewHeight / 3 && mPaddingTop < 0 && stateMove == TouchState_Move.DOWN_NO_OVER) {
moveAnimation(-mPaddingTop, mPaddingTop)
stateView = VIewState.OPEN
dotHideAnim()
}
if (mPaddingTop <= -subViewHeight / 3 && mPaddingTop < 0 && stateMove == TouchState_Move.DOWN_NO_OVER) {
moveAnimation(-mPaddingTop, subViewHeight)
stateView = VIewState.CLOSE
headlayout?.visibility = View.INVISIBLE
dotView?.alpha = 1.0f
}
if (stateMove == TouchState_Move.DOWN_OVER) {
moveAnimation(-mPaddingTop, mPaddingTop)
stateView = VIewState.OPEN
dotHideAnim()
}
if (stateMove == TouchState_Move.UP) {
moveAnimation(-mPaddingTop, subViewHeight)
headlayout?.visibility = View.INVISIBLE
stateView = VIewState.CLOSE
dotView?.alpha = 1.0f
}
mTouchEvent = false
scrollYValue = 0f
mlastY = 0f
}
}
return mTouchEvent
}
/**
* view滚动回弹动画
*/
fun moveAnimation(startY: Int, y: Int) {
mScroller?.startScroll(0, startY, 0, y, 400);
invalidate()
}
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
val currY = mScroller?.getCurrY()
TopLayout?.setPadding(paddingLeft, -currY, paddingRight, mpaddingBottom)
}
invalidate()//刷新view
}
/**
* 触摸状态
* DOWN_NO_OVER 向下滑动但是没有超出view的height值
* DOWN_OVER 向下滑动并且超出了height值
* UP 向上滑动
* NORMAL 无状态
*/
enum class TouchState_Move {
DOWN_NO_OVER, DOWN_OVER, UP, NORMAL
}
/**
* 顶部view的显示状态
* CLOSE 顶部为关闭
* OPEN 顶部为打开状态
*/
enum class VIewState {
CLOSE, OPEN
}
/**
* 顶部view向下平移动画
* @param view
* @param time 动画时间
*/
fun showTodown(view: View, time: Long) {
if (view.visibility != View.VISIBLE) {
val animator1 = ObjectAnimator.ofFloat(view, "translationY", -50f, 0f)
animator1.setInterpolator(AccelerateDecelerateInterpolator())
animator1.setDuration(time).start()
animator1.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator) {}
override fun onAnimationEnd(animator: Animator) {
view.visibility = View.VISIBLE
}
override fun onAnimationCancel(animator: Animator) {}
override fun onAnimationRepeat(animator: Animator) {
}
})
}
}
/**
* 小圆点的隐藏动画
*/
fun dotHideAnim() {
val alpha = dotView?.animate()?.alpha(0f)
alpha?.duration = 400
alpha?.start()
}
}
自定义属性
小圆点
class DotView : View {
internal var percent: Float = 0.toFloat()
internal var maxRadius = 10f
internal var maxDist = 30f
internal var mPaint: Paint
init {
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.color = Color.GRAY
}
public constructor(context: Context) : super(context) {
DotView(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
}
fun setPercent(percent: Float) {
this.percent = percent
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = (width / 2).toFloat()
val centerY = (height / 2).toFloat()
val fl = 255 * percent * 1.5f + 30
mPaint.alpha = if (fl > 255) 255 else fl.toInt()
if (percent <= 0.3f) {
val radius = percent * 3.33f * maxRadius
canvas.drawCircle(centerX, centerY, radius, mPaint)
} else {//画三个个圆
val afterPercent = (percent - 0.3f) / 0.7f
if (afterPercent <= 1) {
val radius = maxRadius - maxRadius / 2f * afterPercent
Log.e("afterPercent--->", afterPercent.toString())
canvas.drawCircle(centerX, centerY, radius, mPaint)
canvas.drawCircle(centerX - afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
canvas.drawCircle(centerX + afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
} else if (afterPercent > 1) {
var d = afterPercent - 1.0
d = if (d > 1) 1.0 else d
val fl = (1 - d * 2) * 255
mPaint.alpha = if (fl < 60) 0 else fl.toInt()
canvas.drawCircle(centerX, centerY, maxRadius / 2, mPaint)
canvas.drawCircle(centerX - maxDist, centerY, maxRadius / 2, mPaint)
canvas.drawCircle(centerX + maxDist, centerY, maxRadius / 2, mPaint)
}
}
}
}