安卓日志中声音出来的关键字是什么?图像出来的关键字是什么?音视频不同步问题的日志分析关键字和思路。
你是要做一个视频播放器的项目吗
我们新建一个类,命名为 NestedOverScrollLayout,继承自 ViewGroup,NestedScrollingParent3,有许多未实现的接口,我们先放着,空实现。
open class NestedOverScrollLayout : ViewGroup, NestedScrollingParent3 {
private var mVelocityTracker = VelocityTracker.obtain()
private var mScroller = Scroller(context)
private var mParentHelper: NestedScrollingParentHelper? = null
private var mTouchSlop: Int = 0
private var mMinimumVelocity: Float = 0f
private var mMaximumVelocity: Float = 0f
private var mCurrentVelocity: Float = 0f
// 阻尼滑动参数
private val mMaxDragRate = 2.5f
private val mMaxDragHeight = 250
private val mScreenHeightPixels = context.resources.displayMetrics.heightPixels
private var mHandler: Handler? = null
private var mNestedInProgress = false
private var mIsAllowOverScroll = true // 是否允许过渡滑动
private var mPreConsumedNeeded = 0 // 在子 View 滑动前,此View需要滑动的距离
private var mSpinner = 0f // 当前竖直方向上 translationY 的距离
private var mReboundAnimator: ValueAnimator? = null
private var mReboundInterpolator = ReboundInterpolator(INTERPOLATOR_VISCOUS_FLUID)
private var mAnimationRunnable: Runnable? = null // 用来实现fling时,先过度滑动再回弹的效果
private var mVerticalPermit = false // 控制fling时等待contentView回到translation = 0 的位置
private var mRefreshContent: View? = null
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
setWillNotDraw(false)
mHandler = Handler(Looper.getMainLooper())
mParentHelper = NestedScrollingParentHelper(this)
ViewConfiguration.get(context).let {
mTouchSlop = it.scaledTouchSlop
mMinimumVelocity = it.scaledMinimumFlingVelocity.toFloat()
mMaximumVelocity = it.scaledMaximumFlingVelocity.toFloat()
}
}
override fun onFinishInflate() {
super.onFinishInflate()
val childCount = super.getChildCount()
for (i in 0 until childCount) {
val childView = super.getChildAt(i)
if (SmartUtil.isContentView(childView)) {
mRefreshContent = childView
break
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var minimumWidth = 0
var minimumHeight = 0
val thisView = this
for (i in 0 until super.getChildCount()) {
val childView = super.getChildAt(i)
if (childView == null || childView.visibility == GONE) continue
if (mRefreshContent == childView) {
mRefreshContent?.let { contentView ->
val lp = contentView.layoutParams
val mlp = lp as? MarginLayoutParams
val leftMargin = mlp?.leftMargin ?: 0
val rightMargin = mlp?.rightMargin ?: 0
val bottomMargin = mlp?.bottomMargin ?: 0
val topMargin = mlp?.topMargin ?: 0
val widthSpec = getChildMeasureSpec(
widthMeasureSpec,
thisView.paddingLeft + thisView.paddingRight + leftMargin + rightMargin, lp.width
)
val heightSpec = getChildMeasureSpec(
heightMeasureSpec,
thisView.paddingTop + thisView.paddingBottom + topMargin + bottomMargin, lp.height
)
contentView.measure(widthSpec, heightSpec)
minimumWidth += contentView.measuredWidth
minimumHeight += contentView.measuredHeight
}
}
}
minimumWidth += thisView.paddingLeft + thisView.paddingRight
minimumHeight += thisView.paddingTop + thisView.paddingBottom
super.setMeasuredDimension(
resolveSize(minimumWidth.coerceAtLeast(super.getSuggestedMinimumWidth()), widthMeasureSpec),
resolveSize(minimumHeight.coerceAtLeast(super.getSuggestedMinimumHeight()), heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val thisView = this
for (i in 0 until super.getChildCount()) {
val childView = super.getChildAt(i)
if (childView == null || childView.visibility == GONE) continue
if (mRefreshContent == childView) {
mRefreshContent?.let { contentView ->
val lp = contentView.layoutParams
val mlp = lp as? MarginLayoutParams
val leftMargin = mlp?.leftMargin ?: 0
val topMargin = mlp?.topMargin ?: 0
val left = leftMargin + thisView.paddingLeft
val top = topMargin + thisView.paddingTop
val right = left + contentView.measuredWidth
val bottom = top + contentView.measuredHeight
contentView.layout(left, top, right, bottom)
}
}
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
return false
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
}
override fun onStopNestedScroll(target: View, type: Int) {
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
}
}
可以看到,类里面声明了很多属性,大部分现在还没有用到,不过现在加上,后面用到这些属性时,会再提到。
在 init()
方法中,作必要的初始化工作:
private fun init() {
mHandler = Handler(Looper.getMainLooper())
mParentHelper = NestedScrollingParentHelper(this)
ViewConfiguration.get(context).let {
mTouchSlop = it.scaledTouchSlop
mMinimumVelocity = it.scaledMinimumFlingVelocity.toFloat()
mMaximumVelocity = it.scaledMaximumFlingVelocity.toFloat()
}
}
比如初始化一个 Handler,这个 handler 主要是为了后文做动画更新 UI 用的,所以传入主线程的 looper 即可。
因为 NestedOverScrollLayout 需要支持嵌套滑动,并在嵌套滑动中扮演 Parent 的角色,所以还需要初始化一个 NestedScrollingParentHelper() 辅助完成嵌套滑动操作。
最后是初始化一些变量:mTouchSlop,mMinimumVelocity,mMaximumVelocity,得到最小滑动距离阈值和滑动速度阈值。
在布局加载结束时,找到可以滚动的 View 作为内容布局,并赋值给 mRefreshContent 属性。
override fun onFinishInflate() {
super.onFinishInflate()
val childCount = super.getChildCount()
for (i in 0 until childCount) {
val childView = super.getChildAt(i)
if (SmartUtil.isContentView(childView)) {
mRefreshContent = childView
break
}
}
}
// SmartUtil
object SmartUtil {
fun isScrollableView(view: View?): Boolean {
return view is AbsListView
|| view is ScrollView
|| view is ScrollingView
|| view is WebView
|| view is NestedScrollingChild
}
fun isContentView(view: View?): Boolean {
return isScrollableView(view)
|| view is ViewPager
|| view is NestedScrollingParent
}
}
onMeasure() 和 onLayout() 就根据 mRefreshContent 进行测量和布局。
为了简单起见,NestedOverScrollLayout 只会包含一个 RecyclerView,所以把这个 RecyclerView 的大小、位置测量了就行了。大家看代码估计也能懂,测量的细节就略过了。
有了这些方法后呢,在布局中使用 NestedOverScrollLayout 就应该有内容显示出来了。
我们新建一个 Activity,修改布局文件,给 RecyclerView 一些假数据:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/eventViewGroup"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.jamgu.home.viewevent.nested.NestedOverScrollLayout
android:id="@+id/eventView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f1227f">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/vRecycler1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center" />
</com.jamgu.home.viewevent.nested.NestedOverScrollLayout>
</FrameLayout>
看看效果。
接下来,我们希望在 RecyclerView 在内容滑动到边界时,将无法消耗的滑动距离,交给 NestedOverScrollLayout 处理,根据上文对 NSP 接口调用时机的分析,RecyclerView 处理完自身滑动后,剩下的距离会传入 NestedOverScrollLayout 的 onNestedScroll() 方法,因此接下来要实现这个方法。