引言
先看以下将要实现目标的效果
解析布局:
1、启动页由于类型不同,因此选用fragment显示
2、fragment根布局采用的VideoViewIjk
3、底部闪烁的上三角MotionalArrowView
4、指示器-IndicatorView
5、幕布式TextView-CurtainTextView
3、4、5都是由RelativeLayout包裹
整个页面能够识别左右上三个方向的手势,根据滑动的方向选用不同的转场动画。
仔细观察的人是否能够察觉在第一页左滑时与原作的不同呢?这是因为原作中使用了ViewPager(嘻嘻别问我怎么知道的),接下来开始讲述编码历程。
正文
顺序按交互与否排序,IndicatorView和CurtainTextView属于有用户交互,MotionalArrowView则没有,最后是交互的实现GestureDetector
-
MotionalArrowView
实现思路是自定义VireGroup将两个三角形上下摆放,设置属性动画改变其透明度。
中途遇到的坑:由于图素选取时尺寸大于控件显示的尺寸,导致了自定义控件内部ImageView不按约束显示,所以在使用此控件时要将其设置成宽小于高的矩形。
fun initView() {
upImageView = ImageView(context)
downImageView = ImageView(context)
upImageView.setImageDrawable(ContextCompat.getDrawable(context, R.mipmap.ic_action_up))
downImageView.setImageDrawable(ContextCompat.getDrawable(context, R.mipmap.ic_action_up))
//如果是正方形,则看不出效果,因为图片太大了
var params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
params.addRule(ALIGN_PARENT_BOTTOM)
addView(upImageView)
addView(downImageView, params)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
showAnimation()
}
}
fun showAnimation() {
var upAnimator = ObjectAnimator.ofFloat(upImageView, "alpha", 0.3f, 1f, 0.3f)
var downAnimator = ObjectAnimator.ofFloat(downImageView, "alpha", 0.3f, 1f, 0.3f)
upAnimator.duration = 1000
downAnimator.duration = 1000
upAnimator.startDelay = 500
var animatorSet = AnimatorSet()
animatorSet.playTogether(upAnimator, downAnimator)
animatorSet.addListener(object : Animator.AnimatorListener {
override fun onAnimationEnd(animation: Animator?) {
animatorSet.startDelay = 500
animatorSet.start()
}
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
})
animatorSet.start()
}
-
IndicatorView
这个就比较简单了,用LinearLayout包裹ImageView,切换时更换ImageView的Drawable。
这里踩了一个kotlin的坑:在typedArray.getDrawable()时,如果控件并没有设置此属性而是采用默认值
//定义
private var normalBG: Drawable
//如果这么写
normalBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_normal)
if (normalBG == null) {
normalBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_normal)
}
//结果
Caused by: java.lang.IllegalStateException: typedArray.getDrawable(R…iew_indicatorView_normal) must not be null
因为定义normalBG时认定不为空,所以当typedArray.getDrawable()
取空值时报异常
如果定义其为private var normalBG: Drawable?
则不报异常
因为我定义的normalBG有默认值,肯定不为空所以改了如下写法(究其原因还是kotlin对于空指针异常的把控,再加上自己kotlin写法的不熟练)
init {
gravity = Gravity.CENTER
var typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndicatorView)
contentMargin = typedArray.getDimensionPixelSize(R.styleable.IndicatorView_indicatorView_margin, 15)
var tempBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_normal)
if (tempBG != null) {
normalBG = tempBG
} else {
normalBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_normal)
}
tempBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_checked)
if (tempBG != null) {
selectBG = tempBG
} else {
selectBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_selected)
}
setSize(typedArray.getInt(R.styleable.IndicatorView_indicatorView_count, 0))
typedArray.recycle()
}
fun setSize(size: Int) {
removeAllViews()
for (i in 0 until size) {
var imageView = ImageView(context)
var params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
params.leftMargin = contentMargin
imageView.scaleType = ImageView.ScaleType.CENTER
if (i == 0) {
imageView.setImageDrawable(selectBG)
} else {
imageView.setImageDrawable(normalBG)
}
addView(imageView, params)
}
}
fun select(position: Int) {
if (position < childCount) {
for (i in 0 until childCount) {
var imageView: ImageView = getChildAt(i) as ImageView
if (position == i) {
imageView.setImageDrawable(selectBG)
} else {
imageView.setImageDrawable(normalBG)
}
}
}
}
-
CurtainTextView
这个就比较叼了!最开始我自定义了TypeTextView控件,通过ValueAnimator.ofInt(0, content.length)
不断setText,能够实现动态打字的效果,但其并不能达到预期的动画效果。因为每一次的setText,TextView本身都要重新测算一下自身,结果就像是一个不断变长的矩形。
而我想要的则是像将矩形上的遮布逐渐揭开的效果。
这让我想到了之前有一篇介绍Span的文章文中虽然效果图和代码并不完全匹配,细读一下代码还是很有帮助的。于是有了一下代码
init {
animator = ObjectAnimator.ofFloat(this, "textAlpha", 0f, 1f)
animator.duration = 1000
animator.addUpdateListener { animation -> text = spannableString }
}
fun setContentText(string: String) {
spannableString = SpannableString(string)
spanList = ArrayList()
for (i in 0 until string.length) {
var span = MutableForegroundColorSpan()
spanList.add(span)
spannableString.setSpan(span, i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
animator.start()
}
class MutableForegroundColorSpan : CharacterStyle(), UpdateAppearance {
var alpha = 0
override fun updateDrawState(tp: TextPaint) {
tp.alpha = alpha
}
}
fun setTextAlpha(alpha: Float) {
var size = spanList.size
var total = size * alpha
for (i in 0 until size) {
var span = spanList.get(i)
if (total >= 1) {
span.alpha = 255
--total
} else {
span.alpha = (255 * total).toInt()
total = 0f
}
}
}
其原理是将要设置的文字全部拆成字符,并对每个字符设置CharacterStyle,通过ObjectAnimator改变每个字符CharacterStyle的透明度。效果就像是原本一行透明的文字逐渐地从第一个字符慢慢显示出来
-
GestureDetector
终于到了文章标题的主旨,由于在fragment中无法重写onTouchEvent所以将重任交给了宿主Activity。
(其实也可以将GestureDetector放到布局中的View上,由于kotlin还是不太顺手所以一直都报View的空指针,现在想想应该是调用的时间不对,无法在onCreate和onCreateView附近的生命周期调用)
gestureDetector = GestureDetector(activity, object : GestureDetector.OnGestureListener {
override fun onShowPress(e: MotionEvent?) {
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}
override fun onDown(e: MotionEvent?): Boolean {
return false
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
var yDifference = e2.y - e1.y
var xDifference = e2.x - e1.x
if (Math.abs(xDifference) > Math.abs(yDifference)) {//横向
if (xDifference > 0) {//right
setPosition(--currentPosition)
} else {
setPosition(++currentPosition)
}
} else {//纵向
if (yDifference > 0) {//down
} else {
goMainLeft(false)
}
}
return true
}
override fun onLongPress(e: MotionEvent?) {
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
return false
}
})
//交接重任
(activity as SplashActivity).gestureDetector = gestureDetector
关键方法是onFling()其中参数e1、e2分别代表滑动的起始点和结束点。以手机屏幕左上角为原点,向右x轴逐渐增加,向下y轴逐渐增加,以此为依据,y值相同时e2.x > e1.x
表示右滑、x值相同时e2.y > e1.y
表示下滑
//Activity
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (gestureDetector != null) {
return gestureDetector.onTouchEvent(event)
}
return super.onTouchEvent(event)
}
至此手势已经获取到了,转场的代码与java并无二致
//由于不会用到退场动画,所以就一样了
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_in_right)
//R.anim.slide_in_right
//在x轴0点处结束即屏幕最左边