Android基础控件——HorizontalScrollView的自定义,完美模仿抖音等短视频拍摄底部切换Tab控件

本文主要是介绍Android基础控件——HorizontalScrollView的自定义,完美模仿抖音等短视频拍摄底部切换Tab控件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

最近在项目中需要用到跟抖音同样的控件效果,找了几个开源的TabLayout控件,要么功能很复杂,要么要自己拓展功能,还要去阅读别人代码,实在是没这个时间折腾。每次遇到找不到第三方的控件时候,就开始撸一个简单的控件,好维护又好拓展,功能也不差,做出来体验也很好

抖音原效果

在这里插入图片描述

模仿效果

在这里插入图片描述

简单使用

在布局上,是用底部Tab控件带动ViewPager的切换,中间的白点只是一个固定的图片而已,不会有任何作用

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:fitsSystemWindows="true"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><com.remo.mobile.framework.widget.UnScrollViewPagerandroid:id="@+id/vp_record"android:layout_width="match_parent"android:layout_height="match_parent" /><!--底部Tab控件--><com.remo.mobile.smallvideo.widget.horScrollerIndication.ScrollerSelectIndicationTextViewandroid:id="@+id/ssitv_tab"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true" /></RelativeLayout><!--中间白点--><ImageViewandroid:layout_width="4dp"android:layout_height="4dp"android:layout_alignParentBottom="true"android:layout_centerHorizontal="true"android:layout_marginBottom="6dp"android:src="@drawable/scroller_textview_indication" />
</RelativeLayout>

从代码上,监听滚动和点击接口后,设置当前的ViewPager界面

ssitv_tab?.setOnPageSelectListenter(object : ScrollerSelectIndication.OnScrollerListener {override fun onPageSelect(position: Int) {vp_record.setCurrentItem(position, false)}
})

功能分析

出于简单考虑,并不会跟系统一样采用View的方式去画出来,而是采用组合View的形式去完成,此处分为两个步骤

  • HorizontalScrollView组合TextView部分
  • 自定义HorizontalScrollView部分

1、HorizontalScrollView组合TextView部分

  • 处理横向滚动组件和TextView之间的关系
  • 巧用Space控件来做空白区域的填充

2、自定义HorizontalScrollView部分

  • Tab的点击
  • 监听Tab的滑动
  • 控制Tab的滑动速度
  • Tab滑动落点的边界判断

实现分析

1、HorizontalScrollView组合TextView部分

此处只是简单的控件,包含HorizontalScrollView和TextView,也是最终外部使用的控件

class ScrollerSelectIndicationTextView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {companion object {const val TAG = "[视频服务-ScrollerSelectIndicationTextView]"}init {initView(context)}private fun initView(context: Context) {LayoutInflater.from(context).inflate(R.layout.layout_scroller_select_indication, this)}fun setOnPageSelectListenter(listener: ScrollerSelectIndication.OnScrollerListener) {ssi_indication?.onPageSelectListener = listener}
}

layout_scroller_select_indication布局包含自定义的HorizontalScrollViewTextView,而Space则是填补前面一段空白的位置,前后各有一段

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="50dp"tools:background="#40000000"><com.remo.mobile.smallvideo.widget.horScrollerIndication.ScrollerSelectIndicationandroid:id="@+id/ssi_indication"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center_vertical"><Spaceandroid:layout_width="250dp"android:layout_height="wrap_content" /><TextViewandroid:id="@+id/tv_album"android:layout_width="80dp"android:layout_height="wrap_content"android:gravity="center"android:text="Album"android:textColor="#40ffffff"android:textSize="14dp" /><TextViewandroid:id="@+id/tv_photo"android:layout_width="80dp"android:layout_height="wrap_content"android:gravity="center"android:text="Photo"android:textColor="#40ffffff"android:textSize="14dp" /><TextViewandroid:id="@+id/tv_video"android:layout_width="80dp"android:layout_height="wrap_content"android:gravity="center"android:text="Video"android:textColor="#40ffffff"android:textSize="14dp" /><TextViewandroid:id="@+id/tv_templates"android:layout_width="80dp"android:layout_height="wrap_content"android:gravity="center"android:text="Templates"android:textColor="#40ffffff"android:textSize="14dp" /><Spaceandroid:layout_width="250dp"android:layout_height="wrap_content" /></LinearLayout></com.remo.mobile.smallvideo.widget.horScrollerIndication.ScrollerSelectIndication>
</RelativeLayout>

2、自定义HorizontalScrollView部分

自定义HorizontalScrollView部分要实现好几个功能,和滑动位置的计算,这块也不是特别复杂,主要包含

  • onSizeChanged获取所有Tab的点和TextView
  • 监听滑动的方案和落点的采集方案
  • 对命中Tab的距离做计算,并移动到Tab的准确位置上
  • 控制滚动的滑动速度

1、定义变量

class ScrollerSelectIndication @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : HorizontalScrollView(context, attrs, defStyleAttr) {var count = 0 //记录当前Tab的个数var tabWidth = 0 //当前Tab的宽度var tabSelectPoint = 0 //当前Tab的命中点位置var selectPoint = mutableListOf<Int>() //记录每个Tab命中的点var selectTextView = mutableListOf<TextView>() //记录每个TextViewvar leftSpace = 0 //当前文本最左边的距离var rightSpace = 0 //当前文本最右边的距离var currentX = -9999 //记录当前滚动的距离var scrollType = ScrollType.IDLE //当前滚动状态var currentSelected = 0 //当前命中位置,从0开始var onPageSelectListener: OnScrollerListener? = null}

2、获取大小

由于我们一开始定义好了是SpaceTextView的配合,那么就可以直接知道它的布局结构,如果后续要拓展成ImageView也是可以的

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)selectPoint.clear() //避免缓存多次利用tabSelectPoint = measuredWidth / 2 //命中的落点是在控件的中间var layout = getChildAt(0) as LinearLayoutLog.i(TAG, "---------------------------------------------")for (i in 0..layout.childCount) {var text = layout.getChildAt(i)if (text is TextView) {var textWidth = text.layoutParams.widthtabWidth = textWidthselectPoint.add(leftSpace + textWidth / 2 + count++ * textWidth) //记录每个Tab命中的点selectTextView.add(text) //记录每个TextView} else if (text is Space) {if (leftSpace == 0) leftSpace = text.layoutParams.width //第一个肯定是左边的Spaceelse if (rightSpace == 0) rightSpace = leftSpace + count * tabWidth //第二个自然是右边的Space}}Log.i(TAG, "当前所有命中Tab的点 = $selectPoint")Log.i(TAG, "当前可见宽度 = $measuredWidth,当前Tab的宽度 = $tabWidth")Log.i(TAG, "当前文本最左边的距离 = $leftSpace,当前文本最右边的距离 = $rightSpace")Log.i(TAG, "---------------------------------------------")
}

3、监听滑动松手后的落点

这里采用了网上的方案,通过Handler一直获取点的变化,当发现点不再移动的时候则判定为滑动停止

enum class ScrollType {IDLE, TOUCH_SCROLL, FLING
}@SuppressLint("LongLogTag")
override fun onTouchEvent(ev: MotionEvent?): Boolean {when (ev?.action) {MotionEvent.ACTION_MOVE -> {scrollType = ScrollType.TOUCH_SCROLLhandler.removeCallbacks(scrollRunnable)}MotionEvent.ACTION_UP -> {handler.post(scrollRunnable)}MotionEvent.ACTION_DOWN -> {}}return super.onTouchEvent(ev)
}@SuppressLint("LongLogTag")
var scrollRunnable: Runnable = object : Runnable {override fun run() {if (scrollX == currentX) {scrollType = ScrollType.IDLEfor (i in 0 until selectPoint.size) {var selectPointWidth = selectPoint[i]// 计算当前点和命中点的距离,如果在某个Tab的一半距离内,那么就自动命中那个Tabif (abs(scrollX + tabSelectPoint - selectPointWidth) <= tabWidth / 2) {Log.i(TAG, "当前命中 = $i")smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)break} else if (scrollX + tabSelectPoint <= leftSpace) { //左边界计算Log.i(TAG, "当前命中最左边")smoothScrollToPosition(selectPoint[0] - tabSelectPoint, 0)break} else if (scrollX + tabSelectPoint >= rightSpace) { //右边界计算Log.i(TAG, "当前命中最右边")smoothScrollToPosition(selectPoint[count - 1] - tabSelectPoint, count - 1)break}}handler.removeCallbacks(this)return} else {scrollType = ScrollType.FLING}currentX = scrollXhandler.postDelayed(this, 50)}
}private fun scrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedscrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)
}private fun smoothScrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedsmoothScrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)
}private fun updateTextView(currentSelected: Int) {for (i in 0 until selectTextView.size) {var textView = selectTextView[i]if (currentSelected == i) {textView.alpha = 1.0ftextView.setTextColor(Color.parseColor("#FFFFFF"))} else {textView.alpha = 0.4ftextView.setTextColor(Color.parseColor("#FFFFFF"))}}
}interface OnScrollerListener {fun onPageSelect(position: Int)
}

4、点击事件

这里直接在获取大小后直接设置点击事件和初始化位置

@SuppressLint1("LongLogTag")
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)......for (i in 0 until selectTextView.size) {selectTextView[i].setOnClickListener {smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)}}postDelayed({scrollToPosition(selectPoint[currentSelected] - tabSelectPoint, currentSelected)}, 20)
}

5、控制滑动速度

当我们做完后发现横向滚动特别快,从其他竞品上看滑动的速度还是比较人性化,有点阻力的感觉,通过设置下面属性增加滑动阻力

override fun fling(velocityX: Int) {super.fling(velocityX / 5)
}

6、源码

package com.remo.mobile.smallvideo.widget.horScrollerIndicationimport android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.Space
import android.widget.TextView
import kotlin.math.abs
import android.annotation.SuppressLint as SuppressLint1class ScrollerSelectIndication @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : HorizontalScrollView(context, attrs, defStyleAttr) {var count = 0 //记录当前Tab的个数var tabWidth = 0 //当前Tab的宽度var tabSelectPoint = 0 //当前Tab的命中点位置var selectPoint = mutableListOf<Int>() //记录每个Tab命中的点var selectTextView = mutableListOf<TextView>() //记录每个TextViewvar leftSpace = 0 //当前文本最左边的距离var rightSpace = 0 //当前文本最右边的距离var currentX = -9999 //记录当前滚动的距离var scrollType = ScrollType.IDLE //当前滚动状态var currentSelected = 0 //当前命中位置,从0开始var onPageSelectListener: OnScrollerListener? = nullcompanion object {const val TAG = "[视频服务-ScrollerSelectIndicationTextView]"}@SuppressLint1("LongLogTag")override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)selectPoint.clear()tabSelectPoint = measuredWidth / 2var layout = getChildAt(0) as LinearLayoutLog.i(TAG, "---------------------------------------------")for (i in 0..layout.childCount) {var text = layout.getChildAt(i)if (text is TextView) {var textWidth = text.layoutParams.widthtabWidth = textWidthselectPoint.add(leftSpace + textWidth / 2 + count++ * textWidth)selectTextView.add(text)} else if (text is Space) {if (leftSpace == 0) leftSpace = text.layoutParams.widthelse if (rightSpace == 0) rightSpace = leftSpace + count * tabWidth}}Log.i(TAG, "当前所有命中Tab的点 = $selectPoint")Log.i(TAG, "当前可见宽度 = $measuredWidth,当前Tab的宽度 = $tabWidth")Log.i(TAG, "当前文本最左边的距离 = $leftSpace,当前文本最右边的距离 = $rightSpace")Log.i(TAG, "---------------------------------------------")for (i in 0 until selectTextView.size) {selectTextView[i].setOnClickListener {smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)}}postDelayed({scrollToPosition(selectPoint[currentSelected] - tabSelectPoint, currentSelected)}, 20)}override fun fling(velocityX: Int) {super.fling(velocityX / 5)}@SuppressLint1("LongLogTag")override fun onTouchEvent(ev: MotionEvent?): Boolean {when (ev?.action) {MotionEvent.ACTION_MOVE -> {scrollType = ScrollType.TOUCH_SCROLLhandler.removeCallbacks(scrollRunnable)}MotionEvent.ACTION_UP -> {handler.post(scrollRunnable)}MotionEvent.ACTION_DOWN -> {}}return super.onTouchEvent(ev)}enum class ScrollType {IDLE, TOUCH_SCROLL, FLING}@SuppressLint1("LongLogTag")var scrollRunnable: Runnable = object : Runnable {override fun run() {if (scrollX == currentX) {scrollType = ScrollType.IDLEfor (i in 0 until selectPoint.size) {var selectPointWidth = selectPoint[i]if (abs(scrollX + tabSelectPoint - selectPointWidth) <= tabWidth / 2) {Log.i(TAG, "当前命中 = $i")smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)break} else if (scrollX + tabSelectPoint <= leftSpace) {Log.i(TAG, "当前命中最左边")smoothScrollToPosition(selectPoint[0] - tabSelectPoint, 0)break} else if (scrollX + tabSelectPoint >= rightSpace) {Log.i(TAG, "当前命中最右边")smoothScrollToPosition(selectPoint[count - 1] - tabSelectPoint, count - 1)break}}handler.removeCallbacks(this)return} else {scrollType = ScrollType.FLING}currentX = scrollXhandler.postDelayed(this, 50)}}private fun scrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedscrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)}private fun smoothScrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedsmoothScrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)}private fun updateTextView(currentSelected: Int) {for (i in 0 until selectTextView.size) {var textView = selectTextView[i]if (currentSelected == i) {textView.alpha = 1.0ftextView.setTextColor(Color.parseColor("#FFFFFF"))} else {textView.alpha = 0.4ftextView.setTextColor(Color.parseColor("#FFFFFF"))}}}interface OnScrollerListener {fun onPageSelect(position: Int)}
}

后续优化

后续让这个控件能实现底部可配置化,将xml布局改造成代码的形式去添加,这样可以减少一层嵌套,这样使用起来更简单一些

1、使用代码的形式

先创建根布局

<RelativeLayoutandroid:id="@+id/ly_indication"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true" />

通过代码的形式,动态添加布局,在参数textList填多少个都能够适配底部Tab的个数

indication = ScrollerSelectIndication(this,textList = mutableListOf("Album", "Photo"))
indication?.onPageSelectListener = object : ScrollerSelectIndication.OnScrollerListener {override fun onPageSelect(position: Int) {pageSelect_Type1(position)}
}
ly_indication = findViewById(R.id.ly_indication)
ly_indication?.addView(indication)

2、源码动态生成布局

在源码中,通过代码的形式,将我们一开始的布局转换成代码,让其适配多个Tab的使用

class ScrollerSelectIndication @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0,textList: List<String>
) : HorizontalScrollView(context, attrs, defStyleAttr) {var count = 0 //记录当前Tab的个数var tabWidth = 0 //当前Tab的宽度var tabSelectPoint = 0 //当前Tab的命中点位置var selectPoint = mutableListOf<Int>() //记录每个Tab命中的点var selectTextView = mutableListOf<TextView>() //记录每个TextViewvar leftSpace = 0 //当前文本最左边的距离var rightSpace = 0 //当前文本最右边的距离var currentX = -9999 //记录当前滚动的距离var scrollType = ScrollType.IDLE //当前滚动状态var currentSelected = 0 //当前命中位置,从0开始var onPageSelectListener: OnScrollerListener? = nullcompanion object {const val TAG = "[视频服务-ScrollerSelectIndicationTextView]"}init {layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 42.dp2px)setBackgroundColor(Color.parseColor("#40000000"))var root = LinearLayout(context)var rootParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)var leftSpace = Space(context)var leftSpaceParams = ViewGroup.LayoutParams(250.dp2px, ViewGroup.LayoutParams.MATCH_PARENT)root.addView(leftSpace, leftSpaceParams)for (text in textList) {var textView = TextView(context)textView.text = texttextView.setTextColor(Color.parseColor("#40ffffff"))textView.textSize = 16ftextView.gravity = Gravity.CENTERvar textViewParams = ViewGroup.LayoutParams(80.dp2px, ViewGroup.LayoutParams.MATCH_PARENT)root.addView(textView, textViewParams)}var rightSpace = Space(context)var rightSpaceParams = ViewGroup.LayoutParams(250.dp2px, ViewGroup.LayoutParams.MATCH_PARENT)root.addView(rightSpace, rightSpaceParams)addView(root, rootParams)}@SuppressLint("LongLogTag")override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)selectPoint.clear()tabSelectPoint = measuredWidth / 2var layout = getChildAt(0) as LinearLayoutLog.i(TAG, "---------------------------------------------")for (i in 0..layout.childCount) {var text = layout.getChildAt(i)if (text is TextView) {var textWidth = text.layoutParams.widthtabWidth = textWidthselectPoint.add(leftSpace + textWidth / 2 + count++ * textWidth)selectTextView.add(text)} else if (text is Space) {if (leftSpace == 0) leftSpace = text.layoutParams.widthelse if (rightSpace == 0) rightSpace = leftSpace + count * tabWidth}}Log.i(TAG, "当前所有命中Tab的点 = $selectPoint")Log.i(TAG, "当前可见宽度 = $measuredWidth,当前Tab的宽度 = $tabWidth")Log.i(TAG, "当前文本最左边的距离 = $leftSpace,当前文本最右边的距离 = $rightSpace")Log.i(TAG, "---------------------------------------------")for (i in 0 until selectTextView.size) {selectTextView[i].setOnClickListener {smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)}}postDelayed({scrollToPosition(selectPoint[currentSelected] - tabSelectPoint, currentSelected)}, 20)}override fun fling(velocityX: Int) {super.fling(velocityX / 5)}@SuppressLint("LongLogTag")override fun onTouchEvent(ev: MotionEvent?): Boolean {when (ev?.action) {MotionEvent.ACTION_MOVE -> {scrollType = ScrollType.TOUCH_SCROLLhandler.removeCallbacks(scrollRunnable)}MotionEvent.ACTION_UP -> {handler.post(scrollRunnable)}MotionEvent.ACTION_DOWN -> {}}return super.onTouchEvent(ev)}enum class ScrollType {IDLE, TOUCH_SCROLL, FLING}@SuppressLint("LongLogTag")var scrollRunnable: Runnable = object : Runnable {override fun run() {if (scrollX == currentX) {scrollType = ScrollType.IDLEfor (i in 0 until selectPoint.size) {var selectPointWidth = selectPoint[i]if (abs(scrollX + tabSelectPoint - selectPointWidth) <= tabWidth / 2) {Log.i(TAG, "当前命中 = $i")smoothScrollToPosition(selectPoint[i] - tabSelectPoint, i)break} else if (scrollX + tabSelectPoint <= leftSpace) {Log.i(TAG, "当前命中最左边")smoothScrollToPosition(selectPoint[0] - tabSelectPoint, 0)break} else if (scrollX + tabSelectPoint >= rightSpace) {Log.i(TAG, "当前命中最右边")smoothScrollToPosition(selectPoint[count - 1] - tabSelectPoint, count - 1)break}}handler.removeCallbacks(this)return} else {scrollType = ScrollType.FLING}currentX = scrollXhandler.postDelayed(this, 50)}}private fun scrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedscrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)}private fun smoothScrollToPosition(scrollX: Int, currentSelected: Int) {this.currentSelected = currentSelectedsmoothScrollTo(scrollX, 0)updateTextView(currentSelected)onPageSelectListener?.onPageSelect(currentSelected)}private fun updateTextView(currentSelected: Int) {for (i in 0 until selectTextView.size) {var textView = selectTextView[i]if (currentSelected == i) {textView.alpha = 1.0ftextView.setTextColor(Color.parseColor("#FFFFFF"))} else {textView.alpha = 0.4ftextView.setTextColor(Color.parseColor("#FFFFFF"))}}}interface OnScrollerListener {fun onPageSelect(position: Int)}
}

这篇关于Android基础控件——HorizontalScrollView的自定义,完美模仿抖音等短视频拍摄底部切换Tab控件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/357943

相关文章

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存