引言
先看以下將要實現目標的效果
解析佈局:
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
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="50.0%p"//x軸在屏幕50%的地方開始 p代表parent
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0.0" />//在x軸0點處結束即屏幕最左邊