随笔 - 129,  文章 - 3,  评论 - 50,  阅读 - 15万
 
摘要: fragment切换动效,基本都是 viewpager+Transformer,但是总有它实现不了的动效,比如从0到3直接过渡,并且过渡需要加上两者的位移效果 慢动作看到,如果是viewpager,从0到3过程是无法直达的,会有0,1,2,3的过渡效果,并且动画时机是不可控的 当然,有个自定义方式可 阅读全文
posted @ 2024-04-29 14:30 翻滚的咸鱼 阅读(86) 评论(0) 推荐(0) 编辑
 
摘要: 很多时候,文本显示是有限制的,有个maxline,然后要求你末尾截取,最后一行末尾是...(xxx文本...),那么只要在你的xml里设置 ellipsize_end 就行了,但是有时候往往需求就是千奇百怪,系统控件无法满足产品的要求 比如下图效果 这个textview在末尾要留一定的空白,因为末尾 阅读全文
posted @ 2021-02-22 09:42 翻滚的咸鱼 阅读(523) 评论(0) 推荐(0) 编辑
 
摘要: 介绍: AspectJ是一个面向切面编程的一个框架,它扩展了java语言,并定义了实现AOP的语法。在将.java文件编译为.class文件时默认使用javac编译工具,AspectJ会有一套符合java字节码编码规范的编译工具来替代javac,在将.java文件编译为.class文件时,会动态的插 阅读全文
posted @ 2019-11-14 11:20 翻滚的咸鱼 阅读(2235) 评论(0) 推荐(0) 编辑
 
摘要: 先上效果图 洒豆子的效果,突发奇想,觉得这个动画挺有意思的,就抽空写了一个玩玩 绘制流程: 定义6个‘’豆子‘’,每个豆子有各自的属性,大小,抛出的速度等,然后控制每个的方向和状态,回弹效果使用差值器 BounceInterpolator package com.fragmentapp.view.b 阅读全文
posted @ 2018-02-08 14:45 翻滚的咸鱼 阅读(652) 评论(0) 推荐(1) 编辑
  2025年1月15日

在 recyclerView 列表中,滑动到边界后,继续滑动,会发现自带一个阻尼效果,但是往往不能满足产品需求,需要自定义

比如拉伸的最大距离,或者拉伸的位置

模仿安卓最近任务列表,列表中item可以上下左右滑动,并且左右下方向滑动到边界后会产生阻尼效果,随着拉伸的距离增大而增大

方案1:这里可以自定义阻尼算法,比如粗糙的方式计算

package com.test.recent_task.utils

import android.util.Log
import kotlin.math.max

/**
 * @author liuzhen
 * 拖拽阻尼
 */
class DragDampingHelper {

    companion object {
        private const val TAG = "DragDampingHelper"
    }

    // 最大拉动值
    private var maxNum = 100f

    // 前一次记录位置
    private var previousNum = 0f

    // 起始位置
    private val startNum = 0f

    // 两次移动间移动的相对距离
    private var deltaNum = 0f

    // 结果
    private var result = 0f

    private fun getDampingNum(y: Float, isUp: Boolean): Float {
        // 抬手或者复位时需要清除缓存数据重新计算
        if (isUp || y == 0f) {
            result = 0f
            previousNum = 0f
            return 0f
        }
        deltaNum = y - previousNum
        previousNum = y

        //计算阻尼
        var distance = y - startNum
        if (distance < 0) {
            distance *= -1f
        }

        var damping = (maxNum - distance) / maxNum
        // 1 ~ 0.1
        damping = max(damping, 0.1f)
        if (y - startNum < 0) {
            damping = 1 - damping
        }

        result += deltaNum * damping
        log("getDampingNum previousNum $previousNum deltaNum $deltaNum result $result damping $damping")
        return result
    }

    /** 阻尼拉动 */
    fun pull(y: Float, isUp: Boolean, maxNum: Float): Float {
        this.maxNum = maxNum * 2
        val result = if (y >= 0) getDampingNum(y, isUp) else {
            getDampingNum(0f, false)
            y
        }
        log("pull y $y isUp $isUp result $result")
        return if (result >= maxNum) maxNum else result
    }

    private fun log(str: String) = Log.w(TAG, str)

}
DragDampingHelper

缺点很明显,如果只是单方向滑动,没什么问题,但是如果按下后来回滑动反人类操作,适配难度较大,这里是直接归零处理,只有往一个方向时才开始计算阻尼

方案2:使用 recyclerView 自带的 EdgeEffect

package com.test.recent_task.utils

import android.animation.ValueAnimator
import android.util.Log
import android.view.animation.DecelerateInterpolator
import android.widget.EdgeEffect
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

/**
 * @author liuzhen
 * 滑动阻尼
 */
class DampingEdgeEffect : RecyclerView.EdgeEffectFactory() {

    private var effect: CustomEdgeEffect? = null

    override fun createEdgeEffect(view: RecyclerView, direction: Int) =
        CustomEdgeEffect(view, direction).apply {
            effect = this
        }

    fun up() = effect?.up()

}

class CustomEdgeEffect(private val recyclerView: RecyclerView, private val direction: Int) :
    EdgeEffect(recyclerView.context) {

    companion object {
        const val TAG = "CustomEdgeEffect"
    }

    private val maxEdgeNum = recyclerView.width / 4.5f

    private val upAnim = ValueAnimator().apply {
        duration = 300
        interpolator = DecelerateInterpolator(2.0f)
        addUpdateListener {
            recyclerView.translationX = it.animatedValue as Float
        }
    }

    override fun onPull(deltaDistance: Float, displacement: Float) {
        super.onPull(deltaDistance, displacement)
        handlePull(deltaDistance)
    }

    private fun handlePull(deltaDistance: Float) {
        val transX = recyclerView.translationX
        val sign = if (direction == RecyclerView.EdgeEffectFactory.DIRECTION_RIGHT) -1 else 1
        val moveX = sign * recyclerView.width * deltaDistance
        // 滑动阻尼
        var offset = 1 - abs(transX) / maxEdgeNum
        if (sign < 0 && transX > 0) {
            offset = 1f
        }
        if (sign > 0 && transX < 0) {
            offset = 1f
        }
        // 1 ~ 0.1
        val damping = max(offset, 0.1f)
        val result = transX + moveX * damping
        log("result $result damping $damping moveX $moveX transX $transX sign $sign")
        recyclerView.translationX = min(result, maxEdgeNum)
    }

    fun up() {
        val transX = recyclerView.translationX
        log("up transX $transX")
        if (abs(transX) > 0) {
            upAnim.cancel()
            upAnim.setFloatValues(transX, 0f)
            upAnim.start()
        }
    }

    private fun log(string: String) = Log.d(TAG, string)

}
DampingEdgeEffect

同时自定义 ItemDecoration,必要时还能对滑动方向进行控制

package com.test.recent_task

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

/**
 * @author liuzhen
 * 横向间距
 */
class SpacingItemDecoration(
    private val firstLeftMargin: Int,
    private val lastRightMargin: Int,
    private val margin: Int
) : RecyclerView.ItemDecoration() {

    private var scaleNum: Int = 0
    private var touchPosition: Int? = null

    fun updateDecoration(scaleNum: Int, touchPosition: Int) {
        this.scaleNum = scaleNum
        this.touchPosition = touchPosition
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)
        when (position) {
            0 -> outRect[firstLeftMargin, 0, margin] = 0
            state.itemCount - 1 -> outRect[margin, 0, lastRightMargin] = 0
            else -> outRect[margin, 0, margin] = 0
        }
        if (touchPosition != null) {
            if ((touchPosition ?: 0) > position) {
                outRect.left += scaleNum
                outRect.right -= scaleNum
            } else if ((touchPosition ?: 0) < position) {
                outRect.left -= scaleNum
                outRect.right += scaleNum
            }
        }
    }

    fun getLeftMargin() = firstLeftMargin

}
SpacingItemDecoration

结合effect通过自定义的方式修改Decoration,达到拉伸的阻尼效果

package com.test.recent_task.view

import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.test.recent_task.R
import com.test.recent_task.ScrollLayoutManager
import com.test.recent_task.SpacingItemDecoration
import com.test.recent_task.utils.DampingEdgeEffect
import com.test.recent_task.utils.DragDampingHelper
import kotlin.math.abs

/**
 * @author liuzhen
 * 任务管理器列表
 */
class RecentTaskListView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

    companion object {
        const val TAG = "RecentTaskListView"
    }

    private val itemDecoration = SpacingItemDecoration(
        resources.getDimension(R.dimen.item_margin_left).toInt(),
        resources.getDimension(R.dimen.item_margin_right).toInt(),
        resources.getDimension(R.dimen.common_dp_28).toInt()
    )

    private var touchView: View? = null
    private val scaleAnim = ValueAnimator().apply {
        repeatCount = 0
        repeatMode = ValueAnimator.RESTART
        duration = 150
        interpolator = AccelerateDecelerateInterpolator()
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            updateDecoration(num)
        }
    }

    private val manager = ScrollLayoutManager(context)
    private var maxNum = resources.getDimension(R.dimen.common_dp_150)
    private var threshold = resources.getDimension(R.dimen.common_dp_50)
    private var dDx = 0F
    private var dDy = 0F

    // 用于列表滑动跟抬起时多次触发动效
    private var isUpScale = false

    var removeTaskListener: ((Int) -> Unit)? = null
    var taskClickListener: ((Int) -> Unit)? = null
    var rootClickListener: (() -> Unit)? = null

    private val helper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
        private var swipeBack = false
        private val dragDampingHelper = DragDampingHelper()
        override fun getMovementFlags(
            recyclerView: RecyclerView,
            viewHolder: ViewHolder
        ): Int {
            val swipeFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
            return makeMovementFlags(0, swipeFlags)
        }

        override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: ViewHolder,
            target: ViewHolder
        ): Boolean {
            Log.d(TAG, "onMove position=${viewHolder.adapterPosition}")
            return false
        }

        override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
            val position = viewHolder.adapterPosition
            Log.d(TAG, "onSwiped position=$position data.size=${adapter?.itemCount}")
            if (position >= 0) {
                swipeBack = false
                removeTaskListener?.invoke(position)
            }
        }

        override fun onChildDraw(
            c: Canvas,
            rv: RecyclerView,
            viewHolder: ViewHolder,
            dX: Float,
            dY: Float,
            actionState: Int,
            isCurrentlyActive: Boolean
        ) {
            val isUp = !isCurrentlyActive
            val offDy = dragDampingHelper.pull(dY, isUp, 100f)
            swipeBack = dY > 100
            setCanScroll(abs(offDy) == 0f || !isCurrentlyActive)
            Log.d(TAG, "onChildDraw swipeBack=$swipeBack")
            super.onChildDraw(c, rv, viewHolder, dX, offDy, actionState, isCurrentlyActive)
        }

        override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int {
            return if (swipeBack) 0 else super.convertToAbsoluteDirection(flags, layoutDirection)
        }

        override fun getSwipeThreshold(viewHolder: ViewHolder) = 0.151f

        override fun getSwipeEscapeVelocity(defaultValue: Float) = 500F
    })

    init {
        addItemDecoration(itemDecoration)
        helper.attachToRecyclerView(this)
        layoutManager = manager
        edgeEffectFactory = DampingEdgeEffect()
        post {
            maxNum = width / 4f
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(e: MotionEvent?): Boolean {
        touch(e)
        return super.onTouchEvent(e)
    }

    private fun touch(e: MotionEvent?) {
        Log.v(TAG, "touch action ${e?.action}")
        e?.apply {
            when (action) {
                MotionEvent.ACTION_DOWN -> {
                    dDx = e.x
                    dDy = e.y
                    touchView = findChildViewUnder(x, y)
                    downScale()
                }

                MotionEvent.ACTION_MOVE -> {
                    // 正常滑动距离
                    val scrollX = dDx - e.x
                    // 滑动超阈值,需要取消动效
                    val isHorizontalScroll = abs(scrollX) > threshold
                    if (isHorizontalScroll && manager.canScrollHorizontally()) {
                        upScale()
                    }
                }

                MotionEvent.ACTION_UP -> {
                    val isScroll = abs(dDx - e.x) < 10 && abs(dDy - e.y) < 10
                    log("click $touchView\ndown x=$dDx y=$dDy, up x=${e.x} y=${e.y}, isScroll $isScroll")
                    (edgeEffectFactory as DampingEdgeEffect).up()
                    setCanScroll(true)
                    upScale()
                    // 滑动时不触发点击
                    if (isScroll) {
                        if (touchView == null) {
                            rootClickListener?.invoke()
                        } else {
                            taskClickListener?.invoke(getChildAdapterPosition(touchView!!))
                        }
                    }
                }
            }
        }
    }

    private fun downScale() {
        log("downScale")
        isUpScale = false
        scaleAnim.cancel()
        touchView?.let {
            scaleAnim.setFloatValues(1f, 0.9f)
            scaleAnim.start()
        }
    }

    private fun upScale() {
        log("upScale isUpScale $isUpScale")
        if (!isUpScale) {
            isUpScale = true
            scaleAnim.cancel()
            touchView?.let {
                scaleAnim.setFloatValues(it.scaleX, 1f)
                scaleAnim.start()
            }
        }
    }

    fun setCanScroll(canScroll: Boolean) {
        log("setCanScroll $canScroll")
        manager.setCanScroll(canScroll)
    }

    /**
     * desc:触摸时更新间距
     * @param scaleNum 缩放度
     */
    private fun updateDecoration(scaleNum: Float) {
        touchView?.apply {
            scaleX = scaleNum
            scaleY = scaleNum
            val touchPosition = getChildAdapterPosition(this)
            log("updateDecoration touchPosition $touchPosition scaleNum $scaleNum")
            val num = (300 * (1 - scaleNum)).toInt()
            itemDecoration.updateDecoration(num, touchPosition)
            invalidateItemDecorations()
        }
    }

    private fun log(str: String) = Log.d(TAG, str)

}
RecentTaskListView

 

posted @ 2025-01-15 16:54 翻滚的咸鱼 阅读(7) 评论(0) 推荐(0) 编辑
  2024年10月27日
摘要: 如果给你一张这样的图片,要求你点击到黑色圆圈时改变点击的圆圈颜色(选中状态)设计UI会给一套选中图,尺寸一致,只有选中的圆圈不同直观的实现方案,使用三个透明View,固定在三个圆圈上方位置,点击时设置点击选中的状态弊端就是如果圆圈多了,需要固定多个View,并且如果圆圈位置发生了改变,每个固定在圆圈 阅读全文
posted @ 2024-10-27 15:17 翻滚的咸鱼 阅读(57) 评论(0) 推荐(0) 编辑
  2024年6月18日
摘要: 场景:多个tab切换,显示不同的Fragment,其中一个Fragment布局是两个RecyclerView,分别位于左右两侧 需求:首次从tabView切换到改tab页时,焦点从tabView首次往下移动时,需要落焦在右侧的第一个item上面 如果按照系统原生逻辑,从tabView下移,可能默认位 阅读全文
posted @ 2024-06-18 15:06 翻滚的咸鱼 阅读(177) 评论(0) 推荐(0) 编辑
  2024年5月28日
摘要: 在 recyclerview 中,想要无论滑动到哪,每次按遥控器落焦,需要落焦在左侧第一个 item 上面,如果不能触屏还好,触屏会导致焦点丢失 根据系统的反馈,如果你滑动了列表,刚好列表的 item 卡在一半的位置,此时系统的落焦规则,不一定会到第一个 之前试过一个效果一般的方案,就是通过 fin 阅读全文
posted @ 2024-05-28 15:29 翻滚的咸鱼 阅读(151) 评论(0) 推荐(0) 编辑
  2024年4月29日
摘要: 效果图,简单的入场退场动效,一般情况是不同view之间去添加动画,某些条件下显然并不符合需求,需要在单个ImageView下进行的 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintL 阅读全文
posted @ 2024-04-29 11:00 翻滚的咸鱼 阅读(61) 评论(0) 推荐(0) 编辑
  2023年10月25日
摘要: Gerrit 需要 review 代码后才能合入,提交到 Gerrit 后,后面再修改一般都是先 Abandoned 第一笔后再重新提交 这样麻烦,并且会产生 Abandoned 记录,Gerrit 是以 Change-Id 作为标识,只要 Change-Id 一致,Gerrit 就认为是同一笔 提 阅读全文
posted @ 2023-10-25 10:25 翻滚的咸鱼 阅读(727) 评论(0) 推荐(0) 编辑
  2023年10月16日
摘要: tv屏中,最难处理的就是焦点问题,而复杂的焦点处理要属应用列表模块了 根据展示的列表,可以翻页,默认焦点处于左上角第一个,此时通过遥控器上下左右可以控制焦点移动位置 焦点所在应用需要有个黄色边框标识,往右移动到边界,自动到下一行,继续往右移动到边界底部自动翻页,往下移动到底部自动翻页 长按应用弹出编 阅读全文
posted @ 2023-10-16 15:51 翻滚的咸鱼 阅读(106) 评论(0) 推荐(0) 编辑
  2023年9月8日
摘要: 最近研发apk校验服务,很多游戏安装包两三个G,如果整个拿去校验,耗时基本二十多秒,这还仅仅是校验的时间,如果加上下载的时间,等待时间太长了 网上很多方案尝试了一下,不太行 1、fast md5 一个第三方库,csdn有人用过说可以提升40%的速度,然后我去试了一下,本来9秒可以完成的校验,变成了2 阅读全文
posted @ 2023-09-08 18:16 翻滚的咸鱼 阅读(1600) 评论(0) 推荐(0) 编辑
  2023年9月7日
摘要: 在Service中使用系统dialog弹框,但是无法覆盖全部,底部菜单依然可以被点击,在某些场景下是不符合需求的 getDialog().getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); 显然是 dialog 的层级 阅读全文
posted @ 2023-09-07 11:20 翻滚的咸鱼 阅读(153) 评论(0) 推荐(0) 编辑
< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

点击右上角即可分享
微信分享提示