Android自定义view从入门到高级

简介

        什么是自定义view?我认为只要不是编译器直接提供可以使用的view,都可以认为是自定义view。自定义view主要分为两大类,第一类自定义view可以通过系统提供的各种view组合,样式变化实现的view。第二类是通过继承view或者ViewGroup类,通过ondraw方法绘制的view,onMeasure来定义视图的测量逻辑,onLayout来定义视图的布局逻辑,以及处理用户交互的方法。

入门-学会通过drawable和组合view实现

       1、通过在drawable目录下创建一个xml文件

        使用shape来实现各种矩形、圆角矩形、椭圆形、圆形、线条等。通过shape定义图形的边框、填充颜色、渐变、圆角半径等属性来创建各种视觉效果。这些shape可以用作背景、边框或者作为图形元素来装饰UI组件。

        2、通过view的组合来实现想要的效果

        1. layout_width:指定视图的宽度。
        2. layout_height:指定视图的高度。
        3. layout_margin:指定视图与其父布局或相邻视图之间的外边距。
        4. layout_gravity:指定视图在其父布局中的对齐方式。
        5. layout_weight:指定视图在线性布局中的权重,用于实现权重分配。
        6. layout_alignParentTop、layout_alignParentBottom、layout_alignParentLeft、                    layout_alignParentRight:用于相对布局,指定视图相对于父布局的对齐方式。

        。。。

初级-通过ondraw绘制简单图形

        1、了解canvas的一些常见操作

操作类型相关API备注
绘制颜色drawColor, drawRGB, drawARGB使用单一颜色填充整个画布
绘制基本形状drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片drawBitmap, drawPicture绘制位图和图片
绘制文本drawText, drawPosText, drawTextOnPath依次为 绘制文字、绘制文字时指定每一个文字位置、依据路径绘制文字
绘制路径drawPath绘制路径。绘制贝塞尔曲线时也须要用到该函数
顶点操作drawVertices, drawBitmapMesh通过对顶点操作能够使图像形变,drawVertices直接对画布作用、 drawBitmapMesh仅仅对绘制的Bitmap作用
画布剪裁clipPath, clipRect设置画布的显示区域
画布快照save, restore, saveLayerXxx, restoreToCount, getSaveCount依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数
画布变换translate, scale, rotate, skew依次为 位移、缩放、 旋转、错切
Matrix(矩阵)getMatrix, setMatrix, concat实际画布的位移。缩放等操作的都是图像矩阵Matrix,仅仅只是Matrix比較难以理解和使用。故封装了一些经常使用的方法。

        2、了解自定义view的一些常见方法

class CustomView:View {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes)

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return super.onTouchEvent(event)
    }
}

        这些构造方法允许你以不同的方式在代码中或者XML布局中创建自定义视图实例,并且提供了不同的参数组合来满足不同的需求。不写对应的构造方法,就不能使用这个方法去创建view。

1. constructor(context: Context?) : super(context):这个构造方法接受一个Context参数,用于在代码中动态创建视图实例。它调用了父类View的对应构造方法。 

2. constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs):这个构造方法接受Context和AttributeSet参数,用于在XML布局文件中使用自定义视图时创建实例。它也调用了父类View的对应构造方法。

3. constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr):这个构造方法接受Context、AttributeSet和defStyleAttr参数,用于在XML布局文件中使用自定义视图时创建实例,并且指定了默认的样式。它同样调用了父类View的对应构造方法。

4. constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes):这个构造方法接受Context、AttributeSet、defStyleAttr和defStyleRes参数,用于在XML布局文件中使用自定义视图时创建实例,并且指定了默认的样式和主题。同样,它调用了父类View的对应构造方法。

        下面的一些方法,是完成一个自定义view的核心方法。

1. onDraw(canvas: Canvas?):这个方法用于定义视图的绘制逻辑。你可以在这里使用Canvas对象来绘制你所需的图形、文本或者其他视觉元素。

2. onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int):这个方法用于定义视图的测量逻辑。在这里,你可以根据测量规格(MeasureSpec)来计算视图的宽度和高度,并通过setMeasuredDimension方法来设置测量结果。

        在这个阶段,系统会通过调用View的measure()方法来测量View的大小。在测量过程中,View会确定自己的宽度和高度,并为其子View提供测量规格。 - View的测量规格通过MeasureSpec来表示,包括三种模式:EXACTLY、AT_MOST和UNSPECIFIED。  EXACTLY模式表示View的大小已经确定,如设置了具体的数值或match_parent属性。 AT_MOST模式表示View的大小不能超过某个边界,如设置了wrap_content属性。  UNSPECIFIED模式表示View的大小没有限制,如在ScrollView中的子View。  在measure()方法中,View会根据测量规格计算自己的测量宽度和高度,并通过setMeasuredDimension()方法设置测量结果。


3. onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int):这个方法用于定义视图的布局逻辑。在这里,你可以根据视图的尺寸和位置来安排视图的子视图的位置。

        在这个阶段,系统会通过调用View的layout()方法来确定View在父容器中的位置。每个View都有自己的布局参数(LayoutParams),父容器会根据这些参数来摆放子View。  在layout()方法中,View会根据父容器传递的布局参数,计算自己的左上角坐标和右下角坐标,然后通过setFrame()方法设置自己的位置。

4. onTouchEvent(event: MotionEvent?):在这个方法中,你可以处理触摸事件,包括按下、移动、抬起等操作。根据需要,你可以返回true表示消费了该事件,或者返回false将事件传递给父视图或其他视图处理。

        如果使用的是一个viewgroup,那么它还有一些额外的常用方法

class CustomViewGroup:ViewGroup {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
        
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        return super.dispatchTouchEvent(ev)
    }
}

 1. onLayout方法:这个方法用于定义子视图在ViewGroup中的布局位置。当ViewGroup需要摆放子视图时,系统会调用这个方法来指定子视图的位置。

2. onInterceptTouchEvent方法:这个方法用于拦截触摸事件。当自身需要拦截触摸事件时,可以重写这个方法来返回true,从而拦截事件的传递。

3. dispatchTouchEvent方法:这个方法用于分发触摸事件。当触摸事件到达ViewGroup时,系统会调用这个方法来分发事件给子视图或者自身进行处理。

通过重写这些方法,你可以实现自定义的布局逻辑、触摸事件处理逻辑,以及事件拦截逻辑,从而实现定制化的ViewGroup行为。

3、实践应用

绘制几个简单常用的图形

绘制扇形,可以通过代码看到,绘制起始位置是3点钟方向,而不是0点方向

class SectorView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    /**扇形的画笔*/
    private var sectorPaint: Paint = Paint()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        initPaint()
        drawSector(canvas)
    }

    /**
     * 绘制扇形
     * */
    private fun drawSector(canvas: Canvas) {
        val rect = RectF(
            10f,
            10f,
            150f,
            150f
        )
        canvas.drawArc(rect,0f,100f,true,sectorPaint)
    }

    /**
     * 初始化画笔
     * */
    private fun initPaint() {
        //当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式, 如圆形样Cap.ROUND,或方形样式Cap.SQUARE
        sectorPaint.strokeCap = Paint.Cap.ROUND
        //设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
        sectorPaint.isAntiAlias = true
    }
}

绘制饼状图就是绘制多个扇形,这里是写死的数据作为演示,需要动态修改扇形区域就要通过数据来计算每一个颜色区域的开始位置,所占的比例。


class SectorView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    /**扇形的画笔*/
    private var sectorPaint: Paint = Paint()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        initPaint()
        drawSector(canvas)
    }

    /**
     * 绘制扇形
     * */
    private fun drawSector(canvas: Canvas) {
        val rect = RectF(
            10f,
            10f,
            150f,
            150f
        )
        canvas.drawArc(rect,0f,100f,true,sectorPaint)
        sectorPaint.color = Color.parseColor("#FFBB86FC")
        canvas.drawArc(rect,100f,50f,true,sectorPaint)
        sectorPaint.color = Color.parseColor("#FF6200EE")
        canvas.drawArc(rect,150f,60f,true,sectorPaint)
        sectorPaint.color = Color.parseColor("#FF03DAC5")
        canvas.drawArc(rect,210f,150f,true,sectorPaint)
    }

    /**
     * 初始化画笔
     * */
    private fun initPaint() {
        //当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式, 如圆形样Cap.ROUND,或方形样式Cap.SQUARE
        sectorPaint.strokeCap = Paint.Cap.ROUND
        //设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
        sectorPaint.isAntiAlias = true
        sectorPaint.color = Color.parseColor("#FF000000")
    }
}

        仪表盘主要是先绘制一个圆,然后绘制刻度,刻度采用for循环,从第一个刻度开始,每次xy坐标按规律增加,特殊刻度就单独定制。这里的仪表盘,我们就通过系统提供的onMeasure方法获取到这个控件宽高,通过宽高来绘制仪表盘,就不会导致绘制的过大,超过显示范围,或者绘制的过小,也可以适配不同的屏幕分辨率。

class ClockView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private var mWidth = 0f
    private var mHeight = 0f
    override fun onDraw(canvas: Canvas) {
        //画外圆
        val paint = Paint()
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 2f
        canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paint)
//        canvas.drawLine(mWidth / 2, mHeight / 2, mWidth, mHeight, paint)
        //画刻度线
        val paint1 = Paint()
        paint.strokeWidth = 3f
        for (i in 0..23) {
            if (i == 0 || i == 6 || i == 12 || i == 18) {
                paint1.strokeWidth = 5f
                paint1.textSize = 30f
                canvas.drawLine(
                    mWidth / 2,
                    mHeight / 2 - mWidth / 2,
                    mWidth / 2,
                    mHeight / 2 - mWidth / 2 + 60,
                    paint1
                )
                val degree = i.toString()
                canvas.drawText(
                    degree,
                    mWidth / 2 - paint1.measureText(degree) / 2,
                    mHeight / 2 - mWidth / 2 + 90,
                    paint1
                )
            } else {
                paint1.strokeWidth = 3f
                paint1.textSize = 15f
                canvas.drawLine(
                    mWidth / 2,
                    mHeight / 2 - mWidth / 2,
                    mWidth / 2,
                    mHeight / 2 - mWidth / 2 + 30,
                    paint1
                )
                val degree = i.toString()
                canvas.drawText(
                    degree,
                    mWidth / 2 - paint1.measureText(degree) / 2,
                    mHeight / 2 - mWidth / 2 + 60,
                    paint1
                )
            }
            canvas.rotate(15f, mWidth / 2, mHeight / 2)
        }
        //画指针
        val paintHour = Paint()
        paintHour.strokeWidth = 20f
        val paintMinute = Paint()
        paintMinute.strokeWidth = 10f
        canvas.save()
        canvas.translate(mWidth / 2, mHeight / 2)
        canvas.drawLine(0f, 0f, 100f, 100f, paintHour)
        canvas.drawLine(0f, 0f, 100f, 200f, paintMinute)
        canvas.restore()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mWidth = measuredWidth.toFloat() - 20
        mHeight = measuredHeight.toFloat() - 20
    }
}

中级-掌握点击事件的传递分发

     1、了解view的事件传递分发机制

         初级的时候,我们只需要掌握绘制一些静态图形技巧,到了中级,我们就需要让这些view动起来,这个动起来效果,我们经常是设置不同的值,再通知view重新绘制,达到视觉上的一种动起来,实际上就是播放PPT。这里我们经常使用invalidate()与postInvalidate(),都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。postInvalidate它是向主线程发送个Message,然后handleMessage时,调用了invalidate()函数。还需要掌握基本view的点击事件传递流程,会解决一些滑动冲突,判断滑动,点击事件的传递等。

        通常情况下,我们写一个viewgroup不会去修改dispatchTouchEvent,因为我们如果直接在dispatchTouchEvent去把点击事件拦截了,其他人在你的viewgroup里面写的子view,就再也没办法获取到点击事件。所以我们一般都是在viewgroup的onInterceptTouchEvent去,viewgroup可以通过判断自己需要拦截的事件去处理并且拦截。同时子view可以通过调用requestDisallowInterceptTouchEvent(true)去阻止父布局拦截。

2、实践应用

        竖直方向两个ScrollView,上下滑动时子ScrollView是无法响应竖直方向的滑动事件,我们需要子ScrollView可以上下滑动。

        解决办法:1、重写子ScrollView的onInterceptTouchEvent方法,在这里添加parent.requestDisallowInterceptTouchEvent(true)方法,就可以让父控件不拦截事件。如果想要优化一下,让子ScrollView滑动到底部的时候父控件继续滑动,那就使用parent.requestDisallowInterceptTouchEvent(false),父控件会根据自身的判断来决定是否拦截。

        如何区别不同的手势,点击,轻滑,长按,拖拽等。

        解决办法:当然,系统提供的方法还是挺香的,Android sdk给我们提供了GestureDetector类

        

private class gesturelistener implements GestureDetector.OnGestureListener{
 
	public boolean onDown(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}
 
	public void onShowPress(MotionEvent e) {
		// TODO Auto-generated method stub
		
	}
 
	public boolean onSingleTapUp(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}
 
	public boolean onScroll(MotionEvent e1, MotionEvent e2,
			float distanceX, float distanceY) {
		// TODO Auto-generated method stub
		return false;
	}
 
	public void onLongPress(MotionEvent e) {
		// TODO Auto-generated method stub
		
	}
 
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		// TODO Auto-generated method stub
		return false;
	}
	
}

这些函数的触发时机:

------GestureDetector.OnGestureListener事件执行顺序------

快速点击屏幕:onDown→onSingleTapUp

稍微慢速的点击屏幕:onDown→onShowPress→onSingleTapUp

长按屏幕:onDown→onShowPress→onLongPress

快速点击屏幕后滑动无惯性:onDown→onScroll→onScroll→onScroll...........

慢速点击屏幕后滑动无惯性:onDown→onShowPress→onScroll→onScroll→onScroll...........

快速点击屏幕后滑动有惯性:onDown→onScroll→onScroll→onScroll...........→onFling

慢速点击屏幕后滑动有惯性:onDown→onShowPress→onScroll→onScroll→onScroll...........→onFling

class MyDoubleTapListener implements GestureDetector.OnDoubleTapListener{

        @Override
        public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
            System.out.println("OnDoubleTapListener:" + "onSingleTapConfirmed");
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent motionEvent) {
            System.out.println("OnDoubleTapListener:" + "onDoubleTap");
            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent motionEvent) {
            System.out.println("OnDoubleTapListener:" + "onDoubleTapEvent");
            return true;
        }
    }

onSingleTapConfirmed:单击事件,用来判定该次点击是单纯的SingleTap而不是DoubleTap。

onDoubleTap:双击事件

onDoubleTapEvent:双击间隔中发生的动作

这玩意的用法也很简单,在ontouch方法里面,把event传进去就行了mGestureDetector.onTouchEvent(event);或者需要缩放判断的时候采用ScaleGestureDetector。

系统方法虽然用起来简单,但是总会出现不满足业务需求的时候,那就只有靠自己手撸了。

我们可以在down的时候记录按下的时间,这个时候就可以在up 的时候通过时间去判断是长按还是短按,或者自己设置不同的点击时长去做不同的处理。在按下到抬起这个时间段,我们可以在move的监听里面去做一些滑动的判断,比如滑动距离很短并且时间也短的时候就抬起了,认为是轻扫,滑动距离长,时间长,认为是滑动拖拽等。主要是通过滑动时间 + 滑动距离去做自己的手势判断。

 

高级-融合贯通

        何谓高级?高级其实就是更懂初级中级的一些操作,更能深入理解里面的逻辑和实现,能够自己通过不同的方式方法去灵活的实现自己的业务需求。列出一些常见的问题或者解决方式,学会自己去构造属于自己的自定义view的世界。

        1、自定义view的时候,适配问题?

        自己写一个控件的时候,宽高不要写死,从onMeasure获取到这个控件的宽高,有些控件需要计算某个刻度或者线条的长度啥的,尽量按照控件宽高的比例去获取,不要写死(采用不同分辨率获取不同的数值的适配方案还是可以写死的)。

        2、点击事件的处理

        自己不需要处理的事件就不要去拦截,只拦截自己需要处理的时候,避免其他人使用你的控件的时候,出现了滑动冲突,他不能修改自己的控件逻辑去解决,必须要改你的控件。

        3、考虑边界问题

        有些时候做一些图表的时候,有些点的位置很高或者很低的时候,再去根据这个点去绘制其它文字,就可能造成文字显示不全等问题。

        4、数据来源

        可以通过xml里面设置自定义属性的声明文件,自定义view的时候通过obtainStyledAttributes方法获取自定义属性的值,并在View的初始化中使用这些值。

        通过自定义view暴露的方法设置数据,通常建议在调用前组装好数据,直接传递给view使用,而不是在view里面去组装数据。

        。。。。

        


                

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

捍卫数据保护:预防和缓解.mallox勒索病毒的威胁

导言: 在当今数字化时代,我们与世界各地的人们通过网络连接在一起,享受着前所未有的便利。然而,随着科技的进步,网络犯罪也在不断演变,.mallox勒索病毒便是其中之一,给无数用户带来了困扰。本文…

【SpringCloud微服务实战07】Sentinel 服务保护

Sentinel 是阿里巴巴开源的一款微服务流量控制组件。主要作用: 流量控制:避免因瞬间高并发流量而导致服务故障流。超时处理、线程隔离、降级熔断:避免因服务故障引起的雪崩问题。一、Sentinel 安装 1、安装Sentinel控制台,下载jar包并启动:Releases alibaba/Sentinel G…

【HiVT】HiVT轨迹预测代码环境配置及训练

0.简介 github项目链接 论文链接 Argoverse 1.1验证集的预期性能是: Models minADE minFDE MR HiVT-64 0.69 1.03 0.10 HiVT-128 0.66 0.97 0.09 1. 拉取代码仓库 git clone https://github.com/ZikangZhou/HiVT.git cd HiVT2. 创建conda环境 conda create -n H…

Java 启动参数 -- 和 -D写法的区别

当我们配置启动1个java 项目通常需要带一些参数 例如 -Denv uat , --spring.profiles.activedev 这些 那么用-D 和 – 的写法区别是什么? 双横线写法 其中这种写法基本上是spring 和 spring 框架独有 最常用的无非是就是上面提到的 --spring.profiles.activede…

LiveGBS流媒体平台GB/T28181功能-海康摄像头国标语音对讲大华摄像头国标语音对讲GB28181语音对讲需要的设备及服务准备

LiveGBS海康摄像头国标语音对讲大华摄像头国标语音对讲GB28181语音对讲需要的设备及服务准备 1、背景2、准备2.1、服务端必备条件(注意)2.2、准备语音对讲设备2.2.1、 大华摄像机2.2.1.1、 配置接入示例2.2.1.2、 配置音频通道编号 2.2.2、 海康摄像机2.…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的危险物品检测系统(深度学习模型+PySide6界面+训练数据集+Python代码)

摘要:本文深入介绍了一个采用深度学习技术的危险物品识别系统,该系统融合了最新的YOLOv8算法,并对比了YOLOv7、YOLOv6、YOLOv5等早期版本的性能。该系统在处理图像、视频、实时视频流及批量文件时,能够准确识别和分类各种危险物品…

jenkins部署go应用 基于docker

丢弃旧的的构建 github 拉取代码 拉取代码排除指定配置文件 报错 环境变量失效 服务器版本为1.21.6 但是一直没有生效

【大模型系列】根据文本检索目标(DINO/DINOv2/GroundingDINO)

文章目录 1 DINO(ICCV2021, Meta)1.1 数据增强1.2 损失函数 2 DINOv2(CVPR2023, Meta)2.1 数据采集方式2.2 训练方法 3 Grounding DINO3.1 Grounding DINO设计思路3.2 网络结构3.2.1 Feature Extraction and Enhancer3.2.2 Language-Guided Query Selection3.2.3 Cross-Modalit…

doris安装(docker方式)

背景 doris有两个进程 fe,处理用户请求,查询,元数据管理,节点管理be,数据存储,查询计划执行 架构图如下: 参考:https://doris.apache.org/zh-CN/docs/get-starting/what-is-apache-doris 1、定义docker-compose文件 version: 3 services:docker-fe:image: "apac…

2024年华为OD机试真题-两个字符串间的最短路径问题-Java-OD统一考试(C卷)

题目描述: 给定两个字符串,分别为字符串A与字符串B。例如A字符串为ABCABBA,B字符串为CBABAC可以得到下图m*n的二维数组,定义原点为(0, 0),终点为(m, n),水平与垂直的每一条边距离为1,映射成坐标系如下图。 从原点(0, 0)到(0, A)为水平边,距离为1,从(0, A)到(A, C)为垂…

10年外贸老手分享,外贸soho要怎么做

经常会有外贸老手想做辞职做soho或者外贸小白想做soho,那么soho到底要怎么怎么才能做好呢?做外贸soho大概会经过下面这6个阶段,构思阶段-草图阶段-尝试阶段-初步阶段-稳定阶段-发展阶段。 不同的阶段因为面对的问题不一样,所以也…

《JAVA与模式》之原型模式

系列文章目录 文章目录 系列文章目录前言一、原型模式的结构二、简单形式的原型模式三、登记形式的原型模式四、克隆满足的条件五、浅克隆和深克隆前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用…

Leetcode 543. 二叉树的直径

题目描述: 给你一棵二叉树的根节点,返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1: 输入:ro…

aardio 调用 C#程序读 Freeplane.mm文件,生成测试用例.csv文件

C# 请参阅:C# 用 System.Xml 读 Freeplane.mm文件,生成测试用例.csv文件 Freeplane 是一款基于 Java 的开源软件,继承 Freemind 的思维导图工具软件,它扩展了知识管理功能,在 Freemind 上增加了一些额外的功能&#x…

从龙珠到Cocos游戏开发中的无限循环滚动背景

引言 从龙珠到Cocos游戏开发中的无限循环滚动背景 近日,鸟山明去世的消息传来,网友们纷纷表示哀悼,这位被外交部哀悼的鸟山明或许你不熟悉,但是他的作品《龙珠》承载着一代人的记忆与青春。 **《龙珠》**作为一部向中国文化致敬的作品,笔者在刷视频时回忆了许多,当看到…

Xcode remove the package dependency

Xcode Version 15.2 (15C500b) 🤔️ 想知道直接右键,这个 Delete 为什么是禁用状态 推荐一下刚上线的 App 熊猫小账本,里面有用到这篇博客讲的内容 熊猫小账本 一个简洁的记账 App,用于记录日常消费开支收入,使用 iCl…

模块化机房:数据中心的未来

随着数字化转型加速,数据中心已成为企业运营的核心。传统的数据中心面临空间利用不足、能源效率低下、扩展性差和维护成本高等问题。模块化机房应运而生,它不仅克服了传统设计的局限,还为数据中心的建设和运营带来了革命性的改变。本文将探讨…

16. UE5 RPG获取GE应用的回调,并根据Tag设置数据显示到窗口

在上一篇介绍了对标签如何在项目中设置,这一篇先讲解一下如何在GE里面使用GameplayTag标签。 之前我在第十一章节中 11. UE5 RPG使用GameplayEffect修改角色属性(二)介绍了一些GE的属性,在UE 5.3版本中,修改的配置方式…

使用零一万物 200K 模型和 Dify 快速搭建模型应用

本篇文章,我们聊聊如何使用 LLM IDE (Dify) 快速搭建一个模型应用,以及使用超长上下文的 200K 模型,完成懒人式的电子书翻译。 准备工具 最近在 GitHub 上看到了前 HuggingFace 员工,前 transformers 核心贡献者之一的 Stas Bek…

通过一篇文章让你了解什么是函数栈帧

函数栈帧的创建和销毁 前言一、什么是函数栈帧二、 理解函数栈帧能解决什么问题三、 函数栈帧的创建和销毁解析3.1 什么是栈3.2 认识相关寄存器和汇编指令相关寄存器eaxebxebpespeip 相关汇编命令 3.3 解析函数栈帧的创建和销毁3.3.1 预备知识3.3.2 函数的调用堆栈3.3.4 准备环…