网站放到iis如何做指向,提供手机自适应网站建设,WordPress导入用户数据,营销型网站制作流程看了我上篇文章Android低代码开发 - 像启蒙和乐高玩具一样的MenuPanel 之后#xff0c;本篇开始讲解代码。
源代码剖析
首先从MenuPanelItemRoot讲起。
package dora.widget.panelinterface MenuPanelItemRoot {/*** 菜单的标题。** return*/var title: String?fun hasTit…看了我上篇文章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它们分别代表什么呢 title就是红圈圈出来的地方。title span就是标题的间隙你直接当成margins比较容易理解。 红框标出来的为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: MutableListMenuPanelItem ArrayList()protected var viewsCache: MutableListView ArrayList()private var onPanelMenuClickListener: OnPanelMenuClickListener? nullprivate var onPanelScrollListener: OnPanelScrollListener? nullprivate val groupInfoList: MutableListGroupInfo ArrayList()private val listenerInfo LinkedListListenerDelegate()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: ListMenuPanelItemget() menuPanelItemsfun getItem(position: Int): MenuPanelItem? {if (position 0 || position menuPanelItems.size - 1) {return null}return menuPanelItems[position]}val itemViewsCache: ListViewget() 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: MutableListMenuPanelItem,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(): MutableListMenuPanelItem {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 MenuPanelprivate 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 version1.0 encodingutf-8?
layout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolstools:context${packageName}.${activityClass}data/dataLinearLayoutandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:orientationverticaldora.widget.DoraTitleBarandroid:idid/titleBarandroid:layout_widthmatch_parentandroid:layout_height50dpapp:dview_titlestring/app_nameandroid:backgroundcolor/colorPrimary/dora.widget.panel.MenuPanelandroid:idid/menuPanelandroid:layout_widthmatch_parentandroid:layout_heightmatch_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(thisSettingsActivity, NewMsgNoticeActivity::class.java)startActivity(intent)}switchLanguage - {// 切换语言spmSelectContent(点击切换语言)val intent Intent(thisSettingsActivity, SetLanguageActivity::class.java)startActivity(intent)}chatFont - {IntentUtils.startActivityWithString(thisSettingsActivity,ChatFontActivity::class.java,KEY_USER_ID,userId)}chatBg - {spmSelectContent(点击聊天背景)IntentUtils.startActivityWithString(thisSettingsActivity,ChatBackgroundActivity::class.java,KEY_USER_ID,userId)}cacheClear - {spmSelectContent(点击缓存清理)IntentUtils.startActivityWithString(thisSettingsActivity,CacheCleanActivity::class.java,KEY_USER_ID,userId)}aboutUs - {spmSelectContent(点击关于我们)IntentUtils.startActivityWithString(thisSettingsActivity,AboutActivity::class.java,KEY_USER_ID,userId)}superUser - {spmSelectContent(点击超级管理员)IntentUtils.startActivityWithString(thisSettingsActivity,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 。