引言
先看以下將要實(shí)現(xiàn)目標(biāo)的效果
解析布局:
1、啟動(dòng)頁由于類型不同檩坚,因此選用fragment顯示
2不翩、fragment根布局采用的VideoViewIjk
3麦轰、底部閃爍的上三角MotionalArrowView
4、指示器-IndicatorView
5闯睹、幕布式TextView-CurtainTextView
3巷挥、4烹卒、5都是由RelativeLayout包裹
整個(gè)頁面能夠識(shí)別左右上三個(gè)方向的手勢(shì)章咧,根據(jù)滑動(dòng)的方向選用不同的轉(zhuǎn)場(chǎng)動(dòng)畫倦西。
仔細(xì)觀察的人是否能夠察覺在第一頁左滑時(shí)與原作的不同呢?這是因?yàn)樵髦惺褂昧薞iewPager(嘻嘻別問我怎么知道的)赁严,接下來開始講述編碼歷程扰柠。
正文
順序按交互與否排序,IndicatorView和CurtainTextView屬于有用戶交互疼约,MotionalArrowView則沒有卤档,最后是交互的實(shí)現(xiàn)GestureDetector
-
MotionalArrowView
實(shí)現(xiàn)思路是自定義VireGroup將兩個(gè)三角形上下擺放,設(shè)置屬性動(dòng)畫改變其透明度程剥。
中途遇到的坑:由于圖素選取時(shí)尺寸大于控件顯示的尺寸劝枣,導(dǎo)致了自定義控件內(nèi)部ImageView不按約束顯示,所以在使用此控件時(shí)要將其設(shè)置成寬小于高的矩形织鲸。
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))
//如果是正方形舔腾,則看不出效果,因?yàn)閳D片太大了
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
這個(gè)就比較簡(jiǎn)單了搂擦,用LinearLayout包裹ImageView琢唾,切換時(shí)更換ImageView的Drawable。
這里踩了一個(gè)kotlin的坑:在typedArray.getDrawable()時(shí)盾饮,如果控件并沒有設(shè)置此屬性而是采用默認(rèn)值
//定義
private var normalBG: Drawable
//如果這么寫
normalBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_normal)
if (normalBG == null) {
normalBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_normal)
}
//結(jié)果
Caused by: java.lang.IllegalStateException: typedArray.getDrawable(R…iew_indicatorView_normal) must not be null
因?yàn)槎xnormalBG時(shí)認(rèn)定不為空,所以當(dāng)typedArray.getDrawable()
取空值時(shí)報(bào)異常
如果定義其為private var normalBG: Drawable?
則不報(bào)異常
因?yàn)槲叶x的normalBG有默認(rèn)值懒熙,肯定不為空所以改了如下寫法(究其原因還是kotlin對(duì)于空指針異常的把控丘损,再加上自己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
這個(gè)就比較叼了!最開始我自定義了TypeTextView控件工扎,通過ValueAnimator.ofInt(0, content.length)
不斷setText徘钥,能夠?qū)崿F(xiàn)動(dòng)態(tài)打字的效果,但其并不能達(dá)到預(yù)期的動(dòng)畫效果肢娘。因?yàn)槊恳淮蔚膕etText呈础,TextView本身都要重新測(cè)算一下自身,結(jié)果就像是一個(gè)不斷變長(zhǎng)的矩形橱健。
而我想要的則是像將矩形上的遮布逐漸揭開的效果而钞。
這讓我想到了之前有一篇介紹Span的文章文中雖然效果圖和代碼并不完全匹配,細(xì)讀一下代碼還是很有幫助的拘荡。于是有了一下代碼
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
}
}
}
其原理是將要設(shè)置的文字全部拆成字符臼节,并對(duì)每個(gè)字符設(shè)置CharacterStyle,通過ObjectAnimator改變每個(gè)字符CharacterStyle的透明度。效果就像是原本一行透明的文字逐漸地從第一個(gè)字符慢慢顯示出來
-
GestureDetector
終于到了文章標(biāo)題的主旨网缝,由于在fragment中無法重寫onTouchEvent所以將重任交給了宿主Activity巨税。
(其實(shí)也可以將GestureDetector放到布局中的View上,由于kotlin還是不太順手所以一直都報(bào)View的空指針粉臊,現(xiàn)在想想應(yīng)該是調(diào)用的時(shí)間不對(duì)草添,無法在onCreate和onCreateView附近的生命周期調(diào)用)
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
關(guān)鍵方法是onFling()其中參數(shù)e1、e2分別代表滑動(dòng)的起始點(diǎn)和結(jié)束點(diǎn)扼仲。以手機(jī)屏幕左上角為原點(diǎn)远寸,向右x軸逐漸增加,向下y軸逐漸增加犀盟,以此為依據(jù)而晒,y值相同時(shí)e2.x > e1.x
表示右滑、x值相同時(shí)e2.y > e1.y
表示下滑
//Activity
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (gestureDetector != null) {
return gestureDetector.onTouchEvent(event)
}
return super.onTouchEvent(event)
}
至此手勢(shì)已經(jīng)獲取到了阅畴,轉(zhuǎn)場(chǎng)的代碼與java并無二致
//由于不會(huì)用到退場(chǎng)動(dòng)畫倡怎,所以就一樣了
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"http://x軸在屏幕50%的地方開始 p代表parent
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0.0" />//在x軸0點(diǎn)處結(jié)束即屏幕最左邊