Android低代码开发 - MenuPanel的源码剖析和基本使用

看了我上篇文章Android低代码开发 - 像启蒙和乐高玩具一样的MenuPanel 之后,本篇开始讲解代码。

源代码剖析

首先从MenuPanelItemRoot讲起。

package dora.widget.panelinterface MenuPanelItemRoot {/*** 菜单的标题。** @return*/var title: String?fun hasTitle(): Boolean/*** 获取标题四周的间距。** @return*/fun getTitleSpan(): Spanfun setTitleSpan(titleSpan: Span)/*** 菜单的上边距。** @return*/var marginTop: Intclass Span {var left = 0var top = 0var right = 0var bottom = 0constructor()/*** 根据水平间距和垂直间距设置四周的间距,常用。** @param horizontal* @param vertical*/constructor(horizontal: Int, vertical: Int) : this(horizontal,vertical,horizontal,vertical)constructor(left: Int, top: Int, right: Int, bottom: Int) {this.left = leftthis.top = topthis.right = rightthis.bottom = bottom}}
}

无论是菜单还是菜单组,都要实现这个接口,这是什么模式啊?对,这是组合模式的应用。树枝节点可以添加若干树叶节点,且它们不会直接产生依赖,而是同时依赖其抽象。这个类里面看到,有title、title span和margin top,它们分别代表什么呢?
截屏2024-05-21 17.png
title就是红圈圈出来的地方。title span就是标题的间隙,你直接当成margins比较容易理解。

截屏2024-05-21 17.png
红框标出来的为margin top。如果有title的情况下,即title不为空以及空字符串,hasTitle()方法会检测出有标题。marginTop是指标题上面的区域。

接下来来看MenuPanel。

package dora.widget.panelimport android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import java.util.LinkedList
import java.util.UUID/*** 通用功能菜单,类似于RecyclerView。*/
open class MenuPanel : ScrollView, View.OnClickListener {/*** 面板的背景颜色,一般为浅灰色。*/private var panelBgColor = DEFAULT_PANEL_BG_COLORprotected var menuPanelItems: MutableList<MenuPanelItem> = ArrayList()protected var viewsCache: MutableList<View> = ArrayList()private var onPanelMenuClickListener: OnPanelMenuClickListener? = nullprivate var onPanelScrollListener: OnPanelScrollListener? = nullprivate val groupInfoList: MutableList<GroupInfo> = ArrayList()private val listenerInfo = LinkedList<ListenerDelegate>()lateinit var panelRoot: FrameLayout/*** 存放Menu和Custom View。*/lateinit var container: LinearLayoutconstructor(context: Context) : super(context) {init(context)}constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {init(context)}constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr) {init(context)}fun removeItem(item: MenuPanelItem): MenuPanel {val position = seekForItemPosition(item)if (position != SEEK_FOR_ITEM_ERROR_NOT_FOUND &&position != SEEK_FOR_ITEM_ERROR_MISS_MENU_NAME) {removeItem(position)} else {Log.e(TAG, "failed to seekForItemPosition,$position")}return this}private fun init(context: Context) {isFillViewport = trueaddContainer(context)}fun setOnPanelMenuClickListener(l: OnPanelMenuClickListener) {onPanelMenuClickListener = l}fun setOnPanelScrollListener(l: OnPanelScrollListener?) {onPanelScrollListener = l}@JvmOverloadsfun parseItemView(item: MenuPanelItem?, isLoadData: Boolean = false): View {val menuView = item!!.inflateView(context)if (isLoadData) {item.initData(menuView)}return menuView}val items: List<MenuPanelItem>get() = menuPanelItemsfun getItem(position: Int): MenuPanelItem? {if (position < 0 || position > menuPanelItems.size - 1) {return null}return menuPanelItems[position]}val itemViewsCache: List<View>get() = viewsCachefun getGroupInfo(item: MenuPanelItem): GroupInfo? {for (groupInfo in groupInfoList) {if (groupInfo.hasItem(item)) {return groupInfo}}return null}/*** 根据item的position移除一个item,此方法被多处引用,修改前需要理清布局层级结构。** @param position* @return*/fun removeItem(position: Int): MenuPanel {val item = menuPanelItems[position]val groupInfo = getGroupInfo(item)val belongToGroup = groupInfo != nullval view = getCacheViewFromPosition(position)if (!belongToGroup) {container.removeView(view)} else {// 属于一个组val menuGroupCard = groupInfo!!.groupMenuCardmenuGroupCard.removeView(view)groupInfo.removeItem(item)// 一个组内的item全部被移除后,也移除掉这个组if (groupInfo.isEmpty) {// 连同title一起移除container.removeView(menuGroupCard)groupInfoList.remove(groupInfo)}}menuPanelItems.removeAt(position)viewsCache.removeAt(position)listenerInfo.removeAt(position)return this}/*** 清空所有item和相关view。*/fun clearAll(): MenuPanel {if (menuPanelItems.size > 0) {menuPanelItems.clear()}container.removeAllViews()viewsCache.clear()groupInfoList.clear()listenerInfo.clear()return this}/*** 移除连续的item。** @param start 第一个item的下标,包括* @param end   最后一个item的下标,包括* @return*/fun removeItemRange(start: Int, end: Int): MenuPanel {for (i in start until end + 1) {removeItem(start)}return this}/*** 从某个位置移除到最后一个item。** @param start 第一个item的下标,包括* @return*/fun removeItemFrom(start: Int): MenuPanel {val end = menuPanelItems.size - 1if (start <= end) {// 有就移除removeItemRange(start, end)}return this}/*** 从第一个item移除到某个位置。** @param end 最后一个item的下标,包括* @return*/fun removeItemTo(end: Int): MenuPanel {val start = 0removeItemRange(start, end)return this}val itemCount: Intget() = menuPanelItems.sizefun addMenuGroup(itemGroup: MenuPanelItemGroup): MenuPanel {val hasTitle = itemGroup.hasTitle()val items = itemGroup.itemsval titleView = TextView(context)titleView.setPadding(itemGroup.getTitleSpan().left, itemGroup.getTitleSpan().top,itemGroup.getTitleSpan().right, itemGroup.getTitleSpan().bottom)titleView.text = itemGroup.titletitleView.textSize = 15ftitleView.setTextColor(DEFAULT_TITLE_COLOR)val menuGroupCard = LinearLayout(context)menuGroupCard.orientation = LinearLayout.VERTICALval lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)lp.topMargin = itemGroup.marginTopmenuGroupCard.layoutParams = lpif (hasTitle) {menuGroupCard.addView(titleView)}for (item in items) {// 清除组内item的边距等applyDefault(item)addMenuToCard(item, menuGroupCard)}container.addView(menuGroupCard)// 保存菜单组信息groupInfoList.add(GroupInfo(items, menuGroupCard))return this}override fun addView(child: View) {if (child !is FrameLayout) {return}if (childCount > 1) {return}super.addView(child)}private fun addContainer(context: Context) {panelRoot = FrameLayout(context)container = LinearLayout(context)container.orientation = LinearLayout.VERTICALcontainer.setBackgroundColor(panelBgColor)panelRoot.addView(container)addView(panelRoot)}fun addMenu(item: MenuPanelItem): MenuPanel {val menuView = bindItemListener(item)if (!item.hasTitle()) {container.addView(menuView)} else {val titleView = TextView(context)titleView.setPadding(item.getTitleSpan().left, item.getTitleSpan().top,item.getTitleSpan().right, item.getTitleSpan().bottom)titleView.text = item.titletitleView.textSize = 15ftitleView.setTextColor(DEFAULT_PANEL_BG_COLOR)val menuCard = LinearLayout(context)menuCard.orientation = LinearLayout.VERTICALmenuCard.addView(titleView)menuCard.addView(menuView)container.addView(menuCard)}return this}private fun addMenuToCard(item: MenuPanelItem, container: LinearLayout) {val menuView = bindItemListener(item)val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)lp.topMargin = item.marginTopmenuView.layoutParams = lpcontainer.addView(menuView)}fun seekForItemPosition(item: MenuPanelItem): Int {for (i in menuPanelItems.indices) {val mpi = menuPanelItems[i]val menu = mpi.menuNameif (menu == "" || item.menuName == "") {return SEEK_FOR_ITEM_ERROR_MISS_MENU_NAME //失去菜单名称}if (menu == item.menuName) {return i}}return SEEK_FOR_ITEM_ERROR_NOT_FOUND}/*** 获取MenuPanel中条目布局中的子控件,推荐使用。** @param position* @param viewId* @return*/fun getCacheChildView(position: Int, viewId: Int): View? {val menuView = getCacheViewFromPosition(position)return menuView?.findViewById(viewId)}/*** 获取item的view,用于修改item的数据。** @param item* @return*/fun getCacheViewFromItem(item: MenuPanelItem): View? {val position = seekForItemPosition(item)return if (position != SEEK_FOR_ITEM_ERROR_NOT_FOUND &&position != SEEK_FOR_ITEM_ERROR_MISS_MENU_NAME) {getCacheViewFromPosition(position)} else null}/*** 获取item的view,用于修改item的数据。** @param position item的位置,从0开始* @return*/fun getCacheViewFromPosition(position: Int): View? {return if (position < viewsCache.size) {viewsCache[position]} else null}protected fun getCacheViewFromTag(tag: String): View? {for (delegate in listenerInfo) {val dtag = delegate.tagif (dtag == tag) {val position = delegate.positionreturn getCacheViewFromPosition(position)}}return null}/*** 绑定item的点击事件。** @param item* @return 绑定成功后返回item的view*/private fun bindItemListener(item: MenuPanelItem): View {menuPanelItems.add(item)//解析Item所对应的布局,并调用item的initDataval menuView = parseItemView(item, true)viewsCache.add(menuView)val tag = UUID.randomUUID().toString().substring(0, 16)menuView.tag = tagval delegate = getListenerInfo(tag)menuView.setOnClickListener(delegate)listenerInfo.add(delegate)return menuView}private fun applyDefault(item: MenuPanelItem) {// item的上边距修改为1dpitem.marginTop =TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f,resources.displayMetrics).toInt()// item去掉标题item.title = ""// item去掉标题边距item.setTitleSpan(MenuPanelItemRoot.Span())}/*** 不是菜单,所以不会影响菜单的点击事件位置,但需要自己处理控件内部的点击事件。** @param view* @param <T>*/fun <T : View> addCustomView(view: T): MenuPanel {container.addView(view)return this}fun <T : View> addCustomView(view: T, index: Int): MenuPanel {container.addView(view, index)return this}fun removeCustomViewAt(position: Int): MenuPanel {if (container.childCount > position) {// 有就移除container.removeViewAt(position)}return this}/*** 样式等参数改变才需要更新,只有类似于addItem、removeItem这样的,不需要调用此方法。*/open fun updatePanel() {requestLayout()}fun getListenerInfo(tag: String): ListenerDelegate {return ListenerDelegate(tag, menuPanelItems.size - 1, this)}class GroupInfo(private var items: MutableList<MenuPanelItem>,var groupMenuCard: LinearLayout) {fun hasItem(item: MenuPanelItem): Boolean {return items.contains(item)}val itemCount: Intget() = items.sizefun addItem(item: MenuPanelItem) {items.add(item)}fun removeItem(item: MenuPanelItem?) {items.remove(item)}val isEmpty: Booleanget() = items.size == 0fun getItems(): MutableList<MenuPanelItem> {return items}}override fun onClick(v: View) {val tag = v.tag as Stringfor (delegate in listenerInfo) {if (delegate.tag == tag) {val clickPos = delegate.positionmenuPanelItems[clickPos].menuName?.let {onPanelMenuClickListener?.onMenuClick(clickPos, v, it)}break}}}fun setPanelBgColor(color: Int): MenuPanel {panelBgColor = colorcontainer.setBackgroundColor(panelBgColor)return this}interface OnPanelMenuClickListener {fun onMenuClick(position: Int, view: View, menuName: String)}override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {super.onScrollChanged(l, t, oldl, oldt)if (scrollY == 0) {onPanelScrollListener?.onScrollToTop()} else if (panelRoot.measuredHeight == scrollY + height) {onPanelScrollListener?.onScrollToBottom()}}interface OnPanelScrollListener {fun onScrollToTop()fun onScrollToBottom()}class ListenerDelegate(val tag: String,val position: Int,private val listener: OnClickListener) : OnClickListener {override fun onClick(v: View) {listener.onClick(v)}}companion object {private const val TAG = "MenuPanel"private const val DEFAULT_PANEL_BG_COLOR = -0xa0a07private const val DEFAULT_TITLE_COLOR = -0x666667private const val SEEK_FOR_ITEM_ERROR_NOT_FOUND = -1private const val SEEK_FOR_ITEM_ERROR_MISS_MENU_NAME = -2}
}

由于它仿RecyclerView的布局,它可以实现少量固定数量的item的高效创建,但不适应于大量item的场景。本来这个控件设计之初就是用在菜单上面的,而业务功能不可能无限多,所以这个问题可以忽略。根据代码我们可以得知,它是一个ScrollView,通常我们宽高都设置成match_parent,上面放一个titlebar,这样就填满了整个内容视图。这里面有addMenu()、addMenuGroup()和addCustomView()三种添加子控件的方法,只有前两种会受框架的约束。也就是说,如果你调用addCustomView()添加非菜单的视图,那么不会有OnPanelMenuClickListener面板菜单点击事件的回调,需要自己处理自身的事件。通过getCacheChildView()getCacheViewFromPosition()这两个方法都是用来更新菜单数据的,它们的区别在于前者是拿item的具体某一个子控件,后者是拿item本身。删除菜单和回调菜单的点击事件会使用到menuName这个属性,所以你在addMenu()的时候,务必保证menuName不重复。无论是添加还是移除菜单,最后都需要调用updatePanel()进行刷新。

package dora.widget.panelimport android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView/*** 自动给最后加一行提示信息,如共有几条记录的菜单面板。*/
class TipsMenuPanel : MenuPanel {private var tips: String? = ""private var tipsColor = -0x666667private var tipsView: TextView? = nullconstructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr)fun setEmptyTips(): TipsMenuPanel {setTips("")return this}fun setTips(tips: String?): TipsMenuPanel {this.tips = tipsreturn this}fun setTipsColor(color: Int): TipsMenuPanel {tipsColor = colorreturn this}override fun updatePanel() {if (tipsView != null) {container.removeView(tipsView)}if (tips != null && tips!!.isNotEmpty()) {tipsView = TextView(context)val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)lp.topMargin = dp2px(context, 5f)lp.bottomMargin = dp2px(context, 5f)tipsView!!.gravity = Gravity.CENTER_HORIZONTALtipsView!!.setTextColor(tipsColor)tipsView!!.layoutParams = lptipsView!!.text = tips// 增加了底部的tipscontainer.addView(tipsView)}super.updatePanel()}private fun dp2px(context: Context, dpVal: Float): Int {return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpVal, context.resources.displayMetrics).toInt()}
}

另外更新其子类TipsMenuPanel的底部提示信息的布局也需要调用updatePanel()方法。

开始使用

先给你们看一下dora-studio-plugin中是如何生成代码的,你就大概知道怎么使用了。

/** Copyright (C) 2022 The Dora Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.dorachat.templates.recipes.app_package.res.layoutfun menuPanelActivityXml(packageName: String,activityClass: String
) = """
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"tools:context="${packageName}.${activityClass}"><data></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><dora.widget.DoraTitleBarandroid:id="@+id/titleBar"android:layout_width="match_parent"android:layout_height="50dp"app:dview_title="@string/app_name"android:background="@color/colorPrimary"/><dora.widget.panel.MenuPanelandroid:id="@+id/menuPanel"android:layout_width="match_parent"android:layout_height="match_parent" /></LinearLayout>
</layout>
"""

以上为生成xml布局。

/** Copyright (C) 2022 The Dora Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.dorachat.templates.recipes.app_package.srcfun menuPanelActivityKt(applicationPackage: String,packageName: String,activityClass: String,bindingName: String,layoutName: String
) = """
package ${packageName}import android.os.Bundleimport dora.BaseActivityimport ${applicationPackage}.R
import ${applicationPackage}.databinding.${bindingName}class ${activityClass} : BaseActivity<${bindingName}>() {override fun getLayoutId(): Int {return R.layout.${layoutName}}override fun initData(savedInstanceState: Bundle?, binding: ${bindingName}) {TODO("Not yet implemented")}
}
"""fun menuPanelActivity(applicationPackage: String,packageName: String,activityClass: String,bindingName: String,layoutName: String
) = """
package ${packageName};import android.os.Bundle;
import androidx.annotation.Nullable;import dora.BaseActivity;import ${applicationPackage}.R;
import ${applicationPackage}.databinding.${bindingName};public class ${activityClass} extends BaseActivity<${bindingName}> {@Overrideprotected int getLayoutId() {return R.layout.${layoutName};}@Overridepublic void initData(@Nullable Bundle savedInstanceState, ${bindingName} binding) {// TODO: Not yet implemented// For Example:// binding.menuPanel.addMenuGroup(//     MenuPanelItemGroup(//         DensityUtils.dp2px(10f),//         NormalMenuPanelItem("menuName", "text", true, "arrowText")//     )// )}
}
"""

以上为生成activity。

Gradle依赖配置
// 添加以下代码到项目根目录下的build.gradle
allprojects {repositories {maven { url "https://jitpack.io" }}
}
// 添加以下代码到app模块的build.gradle
dependencies {implementation 'com.github.dora4:dview-menu-panel:1.0'
}
添加菜单和菜单组
添加菜单
binding.menuPanel.addMenu(NormalMenuPanelItem("menuName", "text", true, "arrowText"))
添加菜单组
binding.menuPanel.addMenuGroup(MenuPanelItemGroup(DensityUtils.dp2px(10f),NormalMenuPanelItem("menuName", "text", true, "arrowText")))

不要无脑copy,参数请自行更换。

修改菜单数据

例如:更新颜色选择菜单的标签颜色。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)if (resultCode == Activity.RESULT_OK) {if (requestCode == 0) {data?.let {val tagColor = it.getStringExtra(KEY_PICKED_COLOR)tagColor?.let {groupTitleColor = tagColor}val tvTag = mBinding.menuPanel.getCacheChildView(1, R.id.tv_menu_panel_color_picker_tag)val color = Color.parseColor(tagColor)val drawable = TagDrawable(color, 0, 0,DensityUtils.dp2px(this, 20f),DensityUtils.dp2px(this, 10f),)if (tvTag != null) {tvTag.background = drawable}}}}
}
设置菜单点击事件
binding.menuPanel.setOnPanelMenuClickListener(object : MenuPanel.OnPanelMenuClickListener {override fun onMenuClick(position: Int, view: View, menuName: String) {when (menuName) {"newMsgNotice" -> {// 新消息通知spmSelectContent("点击新消息通知")val intent = Intent(this@SettingsActivity, NewMsgNoticeActivity::class.java)startActivity(intent)}"switchLanguage" -> {// 切换语言spmSelectContent("点击切换语言")val intent = Intent(this@SettingsActivity, SetLanguageActivity::class.java)startActivity(intent)}"chatFont" -> {IntentUtils.startActivityWithString(this@SettingsActivity,ChatFontActivity::class.java,KEY_USER_ID,userId)}"chatBg" -> {spmSelectContent("点击聊天背景")IntentUtils.startActivityWithString(this@SettingsActivity,ChatBackgroundActivity::class.java,KEY_USER_ID,userId)}"cacheClear" -> {spmSelectContent("点击缓存清理")IntentUtils.startActivityWithString(this@SettingsActivity,CacheCleanActivity::class.java,KEY_USER_ID,userId)}"aboutUs" -> {spmSelectContent("点击关于我们")IntentUtils.startActivityWithString(this@SettingsActivity,AboutActivity::class.java,KEY_USER_ID,userId)}"superUser" -> {spmSelectContent("点击超级管理员")IntentUtils.startActivityWithString(this@SettingsActivity,SuperUserActivity::class.java,KEY_USER_ID,userId)}"logout" -> {spmSelectContent("点击注销登录")// 注销登录dialog!!.show("logout",getString(R.string.are_you_sure_logout))}}}
})

这里注意一点,尽量使用menuName去判断具体是哪一个菜单,而不建议使用position。因为在有删除菜单的情况下,position会错位。spm埋点统计的代码你无需关心。

总结

本篇详细讲解了MenuPanel的核心代码实现及其使用方式,下篇我们演示IDE插件的操作流程。最后不忘点个star支持一下,https://github.com/dora4/dview-menu-panel 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/13872.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大象资讯:PostgreSQL 17 Beta 1 发布!

↑ 关注“少安事务所”公众号&#xff0c;欢迎⭐收藏&#xff0c;不错过精彩内容~ PostgreSQL 全球开发小组 发布于 2024-05-23 PostgreSQL 全球开发小组宣布&#xff0c;PostgreSQL 17 的第一个测试版本现已可供下载。此版本包含 PostgreSQL 17 正式发布时将提供的所有功能的预…

IEN在Web3.0中的性能与安全优势

随着Web3.0的快速发展&#xff0c;优化网络基础设施变得至关重要。智能生态网络&#xff08;Intelligent Ecological Network, IEN&#xff09;作为新一代网络架构&#xff0c;在提升性能与增强安全方面展现出巨大潜力。本文将深入探讨IEN在Web3.0中的技术优势&#xff0c;并展…

高效利用键盘上的 caps lock(大写键)实现中英切换

先看效果 在中文输入环境中&#xff0c;Caps Lock 键经常被忽视&#xff0c;占据了键盘上的黄金位置却很少派上用场。接下来&#xff0c;我将介绍如何将这个闲置的键合理利用&#xff0c;让它变得更加实用。 第一步 设置&#xff1a; 我以五笔为例&#xff1a; 1.输入法默认…

docker如何拉取redis最新镜像并运行

要拉取Docker Hub上最新版本的Redis镜像&#xff0c;您可以使用以下命令&#xff1a; docker pull redis:latest 这里的latest标签会自动获取Redis镜像的最新版本。如果您希望指定一个确切的版本号&#xff0c;可以直接使用该版本号替换latest。例如&#xff0c;要拉取Redis版…

高铁VR虚拟全景展示提升企业实力和形象

步入VR的神奇世界&#xff0c;感受前所未有的汽车展示体验。VR虚拟现实技术以其独特的沉浸式模拟&#xff0c;让你仿佛置身于真实展厅之中&#xff0c;尽情探索汽车的每一处细节。 一、定制化展示&#xff0c;随心所欲 VR汽车虚拟展厅打破空间束缚&#xff0c;让汽车制造商能够…

力扣1809 没有广告的剧集(postgresql)

需求 Table: Playback ----------------- | Column Name | Type | ----------------- | session_id | int | | customer_id | int | | start_time | int | | end_time | int | ----------------- 该表主键为&#xff1a;session_id &#xff08;剧集id&#xff09; customer_…

调试时JSON库一直提示 PDB找不到 使用需要对象文件来进行调试的 /DEBUG:Fastlink生成的

最近调试时一直提示上面的提示框&#xff0c;很是烦躁。 为什么会出现这个错误呢&#xff0c;我一直使用的是/DEBUG。出现原因没有找出来&#xff0c;理论上市使用了/DEBUG:Fastlink这个模式才会出&#xff0c;但是就是一直在报这个错误。 /DEBUG&#xff08;生成调试信息&am…

同旺科技 FLUKE ADPT 隔离版发布 ---- 3

所需设备&#xff1a; 1、FLUKE ADPT 隔离版 内附链接&#xff1b; 应用于&#xff1a;福禄克Fluke 12E / 15BMax / 17B Max / 101 / 106 / 107 应用于&#xff1a;福禄克Fluke 15B / 17B / 18B 总体连接&#xff1a; 连接线&#xff0c;根据自己实际需求而定&#xff1b; …

flink程序本地运行:No ExecutorFactory found to execute the application

1.问题描述 在idea中运行flink job程序出现如下错误&#xff1a; Exception in thread "main" java.lang.IllegalStateException: No ExecutorFactory found to execute the application. at org.apache.flink.core.execution.DefaultExecutorServiceLoader.getE…

微软开发者大会:编程进入自然语言时代、“AI员工”闪亮登场

当地时间周二&#xff0c;美国科技公司微软召开年度Build开发者大会。在CEO纳德拉的带领下&#xff0c;微软各个产品团队再一次展现出惊人的执行力&#xff0c;在发布会上又拿出了接近50个新产品或功能更新。 整场发布会持续了接近两个小时&#xff0c;在这里挑选了一些投资者…

web自动化文件上传弹框处理

目录 文件上传介绍文件上传处理Alert 弹窗介绍Alert 弹窗处理 课程目标 掌握文件上传的场景以及文件上传的处理方式。掌握 Alert 弹窗的场景以及 Alert 弹窗的处理方式。 思考 碰到需要上传文件的场景&#xff0c;自动化测试应该如何解决&#xff1f; 文件上传处理 找到文…

el-switch自动触发更新事件

比如有这样一个列表&#xff0c;允许修改单条数据的状态。希望在更改el-switch状态时能够有个弹框做二次确认&#xff0c;没问题&#xff0c;el-switch已经帮我们想到了&#xff0c;所以它提供了beforeChange&#xff0c;根据beforeChange的结果来决定是否修改状态。一般确认修…

计算机缺失ffmpeg.dll如何修复,五种详细的修复教程分享

当你在使用电脑过程中&#xff0c;突然遇到系统或软件弹出提示信息&#xff0c;告知“ffmpeg.dll文件丢失”怎么办&#xff1f;当电脑提示ffmpeg.dll丢失时&#xff0c;可能会导致一些应用程序无法正常运行或出现错误提示。下面我将介绍5种解决电脑提示ffmpeg.dll丢失的方法。 …

神秘山洞惊现AI绘画至宝Stable Diffusion残卷

最近听到不少大宗门纷纷发声&#xff1a;随着AI神器的现世“程序员职业将不复存在”&#xff0c;“设计图将要失业”。 至此&#xff0c;不少修士开始担忧起来&#xff0c;现出世的AI神器会不会取代掉我辈修士。 其实&#xff0c;至女娲天神创造人类以来&#xff0c;在这漫漫…

Android软件渲染流程

Android软件渲染流程 一.渲染流程1.VSync信号的监听2.VSync信号触发绘制 二.渲染原理1.画布的获取1.1 渲染缓存的初始化1.2 graphics::Canvas的创建1.3 graphics::Canvas与渲染缓存的绑定1.3.1 SkBitmap的初始化1.3.2 SkiaCanvas与SkBitmap的绑定1.3.3 SkCanvas的创建 2.矩形的…

C++ (week4):Linux基础

文章目录 零、Linux简介1.配置环境2.Linux历史3.Linux模型 一、vim二、Linux命令行 (shell命令)1.常用命令与快捷键(1)常用命令①man命令&#xff1a;查看帮助手册 (2)快捷键 2.用户子系统(1)Linux用户(2)用户命令 3.文件子系统命令(1)目录命令1.创建文件&#xff1a;mkdir2.删…

【AI绘画Stable Diffusion】单人LoRA模型训练,打造你的专属模型,新手入门宝典请收藏!

大家好&#xff0c;我是灵魂画师向阳 本期我将教大家如何进行LoRA模型训练&#xff0c;打造你的专属模型&#xff0c;内容比较干&#xff0c;还请耐心看完&#xff01; 随着AIGC的发展&#xff0c;许多传统工作岗位正逐渐被AI取代。同时&#xff0c;AI变革也在创造前所未有的…

ftp是什么,ftp能做什么,ftp有什么用 -----ftp介绍

大家好&#xff0c;我是风屿&#xff0c;今天开始我会给大家介绍一些关于网络方面的配置以及介绍等等&#xff0c;今天是ftp FTP中文名字叫做文件传输协议&#xff0c;英文名字叫做File Transfer Protocol&#xff08;简称为ftp&#xff09; FTP 是因特网网络上历史最悠久的网…

JS 实战 贪吃蛇游戏

一、css 部分 1. 居中 想要开始和暂停两个按钮居中&#xff0c;可以将盒子设置为弹性盒 也可以使用其他方法 【代码】 2. 将父元素设置为相对定位&#xff0c;偏于之后贪吃蛇长长的身子&#xff0c;是以父元素为基点的绝对定位&#xff0c;通过 left 和 top 来控制位置 二、…

富甲美国---沃尔玛创始人山姆·沃尔顿

富甲美国---沃尔玛创始人山姆沃尔顿_山姆沃尔顿是犹太人吗?-CSDN博客文章浏览阅读786次。​1.不断地检讨回顾我们做得好不好或需要改进的&#xff0c;我们从没有对现况满足过。我们会短暂地大肆庆祝成功&#xff0c;然后认真地检讨下次如何能做得更好---不断改进与创新。2我们…