Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >带你造轮子,自定义一个随意拖拽可吸边的View

带你造轮子,自定义一个随意拖拽可吸边的View

作者头像
yechaoa
发布于 2022-09-20 11:56:28
发布于 2022-09-20 11:56:28
62100
代码可运行
举报
文章被收录于专栏:移动开发专栏移动开发专栏
运行总次数:0
代码可运行

1、效果

2、前言

在开发中,随意拖拽可吸边的悬浮View还是比较常见的,这种功能网上也有各种各样的轮子,其实写起来并不复杂,看完本文,你也可以手写一个,而且不到400行代码就能实现一个通用的随意拖拽可吸边的悬浮View组件。

3、功能拆解

4、功能实现

4.1、基础实现

4.1.1、自定义view类

先定义一个FloatView类,继承自FrameLayout,实现构造方法。

创建一个ShapeableImageView,并添加到这个FloatView中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class FloatView : FrameLayout {

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {
        initView()
    }

    private fun initView() {
        val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        layoutParams = lp

        val imageView = ShapeableImageView(context)
        imageView.setImageResource(R.mipmap.ic_avatar)

        addView(imageView)
    }
}

4.1.2、添加到window

在页面的点击事件中,通过DecorView把这个FloatView添加到window中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mBinding.btnAddFloat.setOnClickListener {
    val contentView = this.window.decorView as FrameLayout
    contentView.addView(FloatView(this))
}

来看下效果:

默认在左上角,盖住了标题栏,也延伸到了状态栏,不是很美观。

从这个视图层级关系中可以看出,我们是把FloatView添加到DecorView的根布局(rootView)里面了,实际下面还有一层contentView,contentView是不包含状态栏、导航栏和ActionBar的。

我们改一下添加的层级(content):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
val contentView = this.window.decorView.findViewById(android.R.id.content) as FrameLayout
contentView.addView(FloatView(this))

再看下效果:

此时,是默认显示在状态栏下面了,但还是盖住了标题栏。

这是因为标题栏是在activity的layout中加的toolbar,不是默认的ActionBar,app主题是Theme.Material3.DayNight.NoActionBar,所以显示效果其实是正确的。

手动加上ActionBar看看效果:

这就验证了我们之前的论点了。

不管我们添加的根布局是rootView还是contentView,实际上可能都有需求不要盖住原有页面上的某些元素,这时候可以通过margin或者x/y坐标位置来限制view显示的位置。

4.1.3、视图层级关系

4.2、拖拽

4.2.1、View.OnTouchListener

实现View.OnTouchListener接口,重写onTouch方法,在onTouch方法中根据拖动的坐标实时修改view的位置。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mDownX = event.x
                mDownY = event.y
            }
            MotionEvent.ACTION_MOVE -> {
                offsetTopAndBottom((y - mDownY).toInt())
                offsetLeftAndRight((x - mDownX).toInt())
            }
            MotionEvent.ACTION_UP -> {

            }
        }
        return true
    }
  • MotionEvent.ACTION_DOWN 手指按下
  • MotionEvent.ACTION_MOVE 手指滑动
  • MotionEvent.ACTION_UP 手指抬起

效果:

ok,这就实现随意拖拽了。

4.2.2、动态修改view坐标

上面我们修改view坐标用的是offsetTopAndBottomoffsetLeftAndRight,分别是垂直方向和水平方向的偏移,当然也还有别的方式可以修改坐标

  • view.layout()
  • view.setX/view.setY
  • view.setTranslationX/view.setTranslationY
  • layoutParams.topMargin…
  • offsetTopAndBottom/offsetLeftAndRight

4.2.3、view坐标系

上面我们获取坐标用的是event.x,实际上还有event.rawX,他们的区别是什么,view在视图上的坐标又是怎么定义的?

搞清楚了这些,在做偏移计算时,就能达到事半功倍的效果,省去不必要的调试工作。

一图胜千言:

4.3、吸边

吸边的场景基本可以分为两种:

  1. 上下吸边
  2. 左右吸边

要么左右吸,要么上下吸,上下左右同时吸一般是违背交互逻辑的(四象限),用户也会觉得很奇怪。

吸边的效果其实就是当手指抬起(MotionEvent.ACTION_UP)的时候,根据滑动的距离,以及初始的位置,来决定view最终的位置。

比如默认在顶部,向下滑动的距离不足半屏,那就还是吸附在顶部,超过半屏,则自动吸附在底部,左右同理。

4.3.1、上下吸边

计算公式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1.上半屏:
1.1.滑动距离<半屏=吸顶
1.2.滑动距离>半屏=吸底

2.下半屏:
2.1.滑动距离<半屏=吸底
2.2.滑动距离>半屏=吸顶

先看下效果:

可以看到基础效果我们已经实现了,但是顶部盖住了ToolBar,底部也被NavigationBar遮住了,我们再优化一下,把ToolBarNavigationBar的高度也计算进去。

看下优化后的效果:

这样看起来就好很多了。

上图效果最终代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private fun adsorbTopAndBottom(event: MotionEvent) {
        if (isOriginalFromTop()) {
            // 上半屏
            val centerY = mViewHeight / 2 + abs(event.rawY - mFirstY)
            if (centerY < getScreenHeight() / 2) {
                //滑动距离<半屏=吸顶
                val topY = 0f + mToolBarHeight
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(topY).start()
            } else {
                //滑动距离>半屏=吸底
                val bottomY = getContentHeight() - mViewHeight
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(bottomY.toFloat()).start()
            }
        } else {
            // 下半屏
            val centerY = mViewHeight / 2 + abs(event.rawY - mFirstY)
            if (centerY < getScreenHeight() / 2) {
                //滑动距离<半屏=吸底
                val bottomY = getContentHeight() - mViewHeight
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(bottomY.toFloat()).start()
            } else {
                //滑动距离>半屏=吸顶
                val topY = 0f + mToolBarHeight
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(topY).start()
            }
        }
    }

4.3.2、左右吸边

计算公式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1.左半屏:
1.1.滑动距离<半屏=吸左
1.2.滑动距离>半屏=吸右

2.右半屏:
2.1.滑动距离<半屏=吸右
2.2.滑动距离>半屏=吸左

看下效果:

左右吸边的效果相对上下吸边来说要简单些,因为不用计算ToolBar和NavigationBar,计算逻辑与上下吸边相通,只不过参数是从屏幕高度变为屏幕宽度,Y轴变为X轴。

代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private fun adsorbLeftAndRight(event: MotionEvent) {
        if (isOriginalFromLeft()) {
            // 左半屏
            val centerX = mViewWidth / 2 + abs(event.rawX - mFirstX)
            if (centerX < getScreenWidth() / 2) {
                //滑动距离<半屏=吸左
                val leftX = 0f
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(leftX).start()
            } else {
                //滑动距离<半屏=吸右
                val rightX = getScreenWidth() - mViewWidth
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(rightX.toFloat()).start()
            }
        } else {
            // 右半屏
            val centerX = mViewWidth / 2 + abs(event.rawX - mFirstX)
            if (centerX < getScreenWidth() / 2) {
                //滑动距离<半屏=吸右
                val rightX = getScreenWidth() - mViewWidth
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(rightX.toFloat()).start()
            } else {
                //滑动距离<半屏=吸左
                val leftX = 0f
                animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(leftX).start()
            }
        }
    }

Author:yechaoa

5、进阶封装

为什么要封装一下呢,因为现在的计算逻辑、参数配置都是在FloatView这一个类里,定制化太强反而不具备通用性,可以进行一个简单的抽取封装,向外暴露一些配置和接口,这样在其他的业务场景下也可以复用,避免重复造轮子。

5.1、View封装

5.1.1、BaseFloatView

把FloatView改成BaseFloatView,然后把一些定制化的能力交给子view去实现。

这里列举了3个方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * 获取子view
     */
    protected abstract fun getChildView(): View

    /**
     * 是否可以拖拽
     */
    protected abstract fun getIsCanDrag(): Boolean

    /**
     * 吸边的方式
     */
    protected abstract fun getAdsorbType(): Int

5.1.2、子view

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class AvatarFloatView(context: Context) : BaseFloatView(context) {

    override fun getChildView(): View {
        val imageView = ShapeableImageView(context)
        imageView.setImageResource(R.mipmap.ic_avatar)
        return imageView
    }

    override fun getIsCanDrag(): Boolean {
        return true
    }

    override fun getAdsorbType(): Int {
        return ADSORB_VERTICAL
    }
}

这样稍微抽一下,代码看起来就简洁很多了,只需要配置一下就可以拥有随意拖拽的能力了。

5.2、调用封装

5.2.1、管理类

新建一个FloatManager的管理类,它来负责FloatView的显示隐藏,以及回收逻辑。

设计模式还是使用单例,我们需要在这个单例类里持有Activity,因为需要通过Activity的window获取decorView然后把FloatView添加进去,但是Activity与单例的生命周期是不对等的,这就很容易造成内存泄露。

怎么解?也好办,管理一下activity的生命周期就好了。

在之前分析LifecycleScope源码的文章中有提到关于Activity生命周期的管理,得益于lifecycle的强大,这个问题解起来也变得更简单。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private fun addLifecycle(activity: ComponentActivity?) {
        activity?.lifecycle?.addObserver(mLifecycleEventObserver)
    }

    private var mLifecycleEventObserver = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            hide()
        }
    }

    fun hide() {
        if (::mContentView.isInitialized && mFloatView != null && mContentView.contains(mFloatView!!)) {
            mContentView.removeView(mFloatView)
        }
        mFloatView?.release()
        mFloatView = null
        mActivity?.lifecycle?.removeObserver(mLifecycleEventObserver)
        mActivity = null
    }
  1. 添加生命周期的监听
  2. 在ON_DESTROY的时候处理回收逻辑

5.2.2、FloatManager完整代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@SuppressLint("StaticFieldLeak")
object FloatManager {

    private lateinit var mContentView: FrameLayout
    private var mActivity: ComponentActivity? = null
    private var mFloatView: BaseFloatView? = null

    fun with(activity: ComponentActivity): FloatManager {
        mContentView = activity.window.decorView.findViewById(android.R.id.content) as FrameLayout
        mActivity = activity
        addLifecycle(mActivity)
        return this
    }

    fun add(floatView: BaseFloatView): FloatManager {
        if (::mContentView.isInitialized && mContentView.contains(floatView)) {
            mContentView.removeView(floatView)
        }
        mFloatView = floatView
        return this
    }
    
    fun setClick(listener: BaseFloatView.OnFloatClickListener): FloatManager {
        mFloatView?.setOnFloatClickListener(listener)
        return this
    }
    
    fun show() {
        checkParams()
        mContentView.addView(mFloatView)
    }

    private fun checkParams() {
        if (mActivity == null) {
            throw NullPointerException("You must set the 'Activity' params before the show()")
        }
        if (mFloatView == null) {
            throw NullPointerException("You must set the 'FloatView' params before the show()")
        }
    }

    private fun addLifecycle(activity: ComponentActivity?) {
        activity?.lifecycle?.addObserver(mLifecycleEventObserver)
    }

    private var mLifecycleEventObserver = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            hide()
        }
    }

    fun hide() {
        if (::mContentView.isInitialized && mFloatView != null && mContentView.contains(mFloatView!!)) {
            mContentView.removeView(mFloatView)
        }
        mFloatView?.release()
        mFloatView = null
        mActivity?.lifecycle?.removeObserver(mLifecycleEventObserver)
        mActivity = null
    }
}

5.2.3、调用方式

  • 显示
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FloatManager.with(this).add(AvatarFloatView(this)).show()
  • 隐藏
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FloatManager.hide()
  • 带点击事件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FloatManager.with(this).add(AvatarFloatView(this))
    .setClick(object : BaseFloatView.OnFloatClickListener {
        override fun onClick(view: View) {
            Toast.makeText(this@FloatViewActivity, "click", Toast.LENGTH_SHORT).show()
        }
    })
    .show()

6、Github

https://github.com/yechaoa/MaterialDesign

7、最后

写作不易,且看且珍惜啊喂~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
VB.NET 创建ASP.NET WebAPI及应用(一)
应用程序接口(API,Application Programming Interface)是基于编程语言构建的结构,使开发人员更容易地创建复杂的功能。它们抽象了复杂的代码,并提供一些简单的接口规则直接使用。
办公魔盒
2021/12/06
3.7K0
VB.NET 创建ASP.NET WebAPI及应用(一)
使用VB.NET 创建.NET6 Minimal Api[号称最小的Webapi](全网首发)
Minimal API是.Net 6中新增的模板,借助C# 10的一些特性以最少的代码运行一个Web服务。本文使用VB.NET ,完成一个简单的Minimal Api项目的开发。(估计是全网首发吧),找遍了百度,必应,Github都没找到VB.NET版本的Minimal Api项目,找到的都是C#的项目,毕竟专为C#10而生!!!
办公魔盒
2022/01/14
3.9K1
使用VB.NET 创建.NET6 Minimal Api[号称最小的Webapi](全网首发)
[接上篇]在Window10/11的Linux子系统Docker上部署VB.NET Asp.Net Core WebAPI应用
宝塔面板Linux系统通过Docker部署VB.NET Asp.Net Core WebAPI应用
办公魔盒
2023/03/02
9950
[接上篇]在Window10/11的Linux子系统Docker上部署VB.NET Asp.Net Core WebAPI应用
VB.NET ASP.NET WebAPI及应用(番外篇)Swagger接口文档自动生成
WebAPI应用集合列表 VB.NET 创建ASP.NET WebAPI及应用(一) VB.NET 创ASP.NET WebAPI及应用(二) IIS和MYSQL安装 VB.NET ASP.NET W
办公魔盒
2021/12/31
2.3K0
VB.NET ASP.NET WebAPI及应用(番外篇)Swagger接口文档自动生成
VB.NET WinForm自托管WebApi服务器(接上期的视频)
  本篇文章是接着上期的《VB.NET 结合 B4A 开发进行远程查图报共上传数据功能》的一个延展性,本期主要介绍 WebApi 自托管于 WinForm 程序上的对外作为数据服务接口的一个简单示例!想跟深入研究的大佬们自行度娘咯;本文只做个抛砖引玉;
办公魔盒
2021/06/25
2.1K0
VB.NET WinForm自托管WebApi服务器(接上期的视频)
VB.NET ASP.NET WebAPI及应用(三)使用Mysql数据库简单的用户登录注册取数据WebAPI
一,首先我们要在数据库里面创建一个简单用户表(角色表等其他表需要的自行创建,这里只做演示,就创建一个简单的用户表)
办公魔盒
2021/12/06
1.9K0
VB.NET ASP.NET WebAPI及应用(三)使用Mysql数据库简单的用户登录注册取数据WebAPI
VB.NET ASP.NET WebAPI及应用(四)[完结] 部署与客户端连接
1.1 打开第三章节的项目,右键"发布",选择"文件夹"进行发布,文件夹就使用第二章IIS部署的网站根目录"F:\IIS_ROOT\Home",发布成功后会在根目录下看到相应bin文件夹和一下配置文件不用理会!!!!!!!!!!!!!
办公魔盒
2021/12/08
3.7K0
VB.NET ASP.NET WebAPI及应用(四)[完结] 部署与客户端连接
.NET Core微服务之ASP.NET Core on Docker
  Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。
Edison Zhou
2018/07/31
1.8K0
.NET Core微服务之ASP.NET Core on Docker
ASP.NET Core Swagger接入使用IdentityServer4 的 WebApi
是这样的,我们现在接口使用了Ocelot做网关,Ocelot里面集成了基于IdentityServer4开发的授权中心用于对Api资源的保护。问题来了,我们的Api用了SwaggerUI做接口的自文档,那就蛋疼了,你接入了IdentityServer4的Api,用SwaggerUI调试、调用接口的话,妥妥的401,未授权啊。那有小伙伴就会说了,你SwaggerUI的Api不经过网关不就ok了?诶,好办法。但是:
乔达摩@嘿
2020/09/11
1.6K0
ASP.NET Core Swagger接入使用IdentityServer4 的 WebApi
.net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍
在Asp.Net Core 上面由于现在前后端分离已经是趋势,所以asp.net core MVC用的没有那么多,主要以WebApi作为学习目标。
SpringSun
2023/06/09
2.2K0
.net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍
.NET Core微服务之ASP.NET Core on Docker
  Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。
星哥玩云
2022/07/28
1.1K0
.NET Core微服务之ASP.NET Core on Docker
ASP.NET Core on K8S学习初探(3)部署API到K8S
在上一篇《基本概念快速一览》中,我们把基本的一些概念快速地简单地不求甚解地过了一下,本篇开始我们会将ASP.NET Core WebAPI部署到K8S,从而结束初探的旅程。
Edison Zhou
2019/07/09
1.2K0
ASP.NET Core on K8S学习初探(3)部署API到K8S
在上一篇《基本概念快速一览》中,我们把基本的一些概念快速地简单地不求甚解地过了一下,本篇开始我们会将ASP.NET Core WebAPI部署到K8S,从而结束初探的旅程。
心莱科技雪雁
2019/07/12
5680
ASP.NET Core on K8S学习初探(3)部署API到K8S
ASP.NET Core 3.0 迁移避坑指南
.NET Core 3.0将会在 .NET Conf 大会上正式发布,截止今日发布了9个预览版,改动也是不少,由于没有持续关注,今天将前面开源的动态WebApi项目迁移到.NET Core 3.0还花了不少时间踩坑,给大家分享一下我在迁移过程中遇到的坑。迁移的版本是当前Release最新版本 .NET Core 2.2 到 .NET Core 3.0 Preview 9。
晓晨
2019/09/12
1K0
ASP.NET Core 3.0 迁移避坑指南
创建API服务最小只要4行代码!!!尝新体验ASP.NET Core 6预览版本中的最小Web API(minimal APIS)新特性
本文首发于《创建API服务最小只要4行代码!!!尝新体验ASP.NET Core 6预览版本中的最小Web API(minimal APIS)新特性》
Rector
2021/08/19
5.4K0
创建API服务最小只要4行代码!!!尝新体验ASP.NET Core 6预览版本中的最小Web API(minimal APIS)新特性
ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
将 Swagger 生成器添加到 Startup.ConfigureServices 方法中的服务集合中:
依乐祝
2018/09/18
3.4K0
ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
ASP.NET Core 实战:将 .NET Core 2.0 项目升级到 .NET Core 2.1
   最近一两个星期,加班,然后回去后弄自己的博客,把自己的电脑从 Windows 10 改到 Ubuntu 18.10 又弄回 Windows 10,原本计划的学习 Vue 中生命周期的相关知识目前也没有任何的进展,嗯,罪过罪过。看了眼时间,11月也快要结束了,准备补上一篇如何将我们的 .NET Core 2.0 版本的程序升级到 .NET Core 2.1 版本,好歹也算多学了一点。
程序员宇说
2019/09/11
1.2K0
ASP.NET Core 实战:将 .NET Core 2.0 项目升级到 .NET Core 2.1
这样入门asp.net core,如何
本文章主要说明asp.net core的创建和简单使用。 一、使用到的命令 dotnet new :创建项目(解决方案,类库,单元测试等),如:dotnet new web dotnet add package 添加一个nuget的引用 dotnet test:运行测试 dotnet build:编译项目 dotnet sln add:将项目添加到解决方案 dotnet add reference:对此项目添加项目引用 二、建立空项目 在测试目录下运行 dotnet new web -n baseWeb,创
sam dragon
2018/03/28
2.2K0
这样入门asp.net core,如何
ASP.NET Core on K8S深入学习(2)部署过程解析与Dashboard
上一篇《K8S集群部署》中搭建好了一个最小化的K8S集群,这一篇我们来部署一个ASP.NET Core WebAPI项目来介绍一下整个部署过程的运行机制,然后部署一下Dashboard,完成可视化管理。本篇已加入了《.NET Core on K8S学习实践系列文章索引》,更多内容请到索引中查看。
Edison Zhou
2019/08/05
1.3K0
ASP.NET Core on K8S深入学习(2)部署过程解析与Dashboard
ASP.NET Core 奇淫技巧之动态WebApi
接触到动态WebApi(Dynamic Web API)这个词的已有几年,是从ABP框架里面接触到的,当时便对ABP的这个技术很好奇,后面分析了一波,也尝试过从ABP剥离一个出来作为独立组件来使用,可是后来因与ABP依赖太多而放弃。十几天前朋友 熊猫 将这部分代码(我和他在搞事情)成功的从 ABP 中剥离出来并做了一个简单Demo扔给我,经过这么久(实在是太懒^_^)终于经过一些修改、添加功能、封装,现在已经能作为一个独立组件使用,项目开源在Github(https://github.com/dotnetauth/Panda.DynamicWebApi),希望觉得有用的朋友能给一个 Star 支持一下。
晓晨
2019/06/15
2K0
推荐阅读
相关推荐
VB.NET 创建ASP.NET WebAPI及应用(一)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验