注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

因为使用 try-cache-finally 读取文件 ,导致我被开除了.(try-with-resources 的使用方法)

前些天项目经理找到我说,阿杰,你过来一下,我这有个小方法,你帮我写一下 需求: 提供一个文本文件,按行读取,然后给出一个回调函数,可以由调用者去实现对每行的处理. 我就想,你这不是瞧不起我吗.5分钟搞定!!嘴里却说,你这个有点难,我需要研究下大概今天下班前...
继续阅读 »

前些天项目经理找到我说,阿杰,你过来一下,我这有个小方法,你帮我写一下



  • 需求: 提供一个文本文件,按行读取,然后给出一个回调函数,可以由调用者去实现对每行的处理.


我就想,你这不是瞧不起我吗.5分钟搞定!!嘴里却说,你这个有点难,我需要研究下大概今天下班前能完成.



5分钟过去了----> 代码完成




摸鱼3小时 ----> ok 代码一发,收工准备下班



public void clean2(String path, Consumer<String> consumer){
FileReader fileReader = null;
BufferedReader br = null;
try{
fileReader = new FileReader(path);
br = new BufferedReader(fileReader);
String line;
while((line = br.readLine()) != null ){
consumer.accept(line);
}
}catch (IOException e){
// do
}finally {
try {
if (br != null){
br.close();
}
if (fileReader != null){
fileReader.close();
}
} catch (IOException e) {
// do
}
}
}

项目经理 😶😶😶😶: 你tm明天别来了,自己去财务把这个月的结了,3行代码就写完的功能写成这个鬼样子.


那我就想啊,我写的这么完美,那凭什么开除我,经过我九九八十一天的苦思冥想,终于找到了问题的原因!!


try-cache-finally


try-finally 是java SE7之前我们处理一些需要关闭的资源的做法,无论是否出现异常都要对资源进行关闭。*


如果try块和finally块中的方法都抛出异常那么try块中的异常会被抑制(suppress),只会抛出finally中的异常,而把try块的异常完全忽略。


这里如果我们用catch语句去获得try块的异常,也没有什么影响,catch块虽然能获取到try块的异常但是对函数运行结束抛出的异常并没有什么影响。


try-with-resources



try-with-resources语句能够帮你自动调用资源的close()函数关闭资源不用到finally块。


前提是只有实现了Closeable接口的才能自动关闭



public void clean(String path, Consumer<String> consumer) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line;
while((line = br.readLine()) != null ){
consumer.accept(line);
}
}
}

这是try-with-resources语句的结构,在try关键字后面的( )里new一些需要自动关闭的资源。


这个时候如果方法 readLine 和自动关闭资源的过程都抛出异常,那么:




  1. 函数执行结束之后抛出的是try块的异常,而try-with-resources语句关闭过程中的异常会被抑制,放在try块抛出的异常的一个数组里。(上面的非try-with-resources例子抛出的是finally的异常,而且try块的异常也不会放在fianlly抛出的异常的抑制数组里)




  2. 可以通过异常的public final synchronized Throwable[] getSuppressed() 方法获得一个被抑制异常的数组。




  3. try块抛出的异常调用getSuppressed()方法获得一个被它抑制的异常的数组,其中就有关闭资源的过程产生的异常。




try-with-resources 语句能放多个资源,使用 ; 分割


try (
BufferedReader br = new BufferedReader(new FileReader(path));
ZipFile zipFile = new ZipFile("");
FileReader fileReader = new FileReader("");
) {

}

最后任务执行完毕或者出现异常中断之后是根据new的反向顺序调用各资源的close()的。后new的先关。


try-with-resources 语句也可以有 catch 和 finally 块


public void clean3(String path, Consumer<String> consumer){
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
System.out.println("RuntimeException 前");
int a = 1/0;
System.out.println("RuntimeException 后");
}catch (RuntimeException e){
System.out.println("抛出 RuntimeException");
}catch (IOException e){
System.out.println("抛出 RuntimeException");
}finally {
System.out.println("finally");
}
}

RuntimeException 前
抛出 RuntimeException
finally

作者:全村最野的狗
链接:https://juejin.cn/post/7198847208564015164
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Compose自定义View——宇智波斑写轮眼

本章节是Compose自定义绘制的第二章,画的是一个之前设计给的一个比较复杂的,设计所谓的会呼吸的动画,那时候实现花了蛮长的时间,搬着电脑跟设计一帧一帧地对,没多久后来需求就被拿掉了,至于文章的标题哈哈随意起了一个,长得有点像而已。 Compose的实现,图形...
继续阅读 »

本章节是Compose自定义绘制的第二章,画的是一个之前设计给的一个比较复杂的,设计所谓的会呼吸的动画,那时候实现花了蛮长的时间,搬着电脑跟设计一帧一帧地对,没多久后来需求就被拿掉了,至于文章的标题哈哈随意起了一个,长得有点像而已。


Compose的实现,图形本身跟上一章节的LocationMarker其实差不太多,倒过来了而已,调整了P1跟P3, 基本图形的Path,这里不再做介绍,读者也可以去看代码实现。主要介绍一下动画吧。


首先看一下gif动图:


waterDrop_AdobeExpress .gif


整个图形分三层,最底层是灰色的背景,没有动画实现。


第二层是一个层变的动画,每层有个delay的不同延迟,对alpha最一个ObjectAnimator.ofFloat(water1, "alpha", 0f, 0.5f, 0.2f, 1f)渐变的动画,0.5f 到0.2f, 再到1f这个地方展现出所谓的呼吸的感觉。Compose目前写的不多,有些冗余代码没有抽象,先实现了功能效果。


@Composable
fun drawWaterDrop(){
 val waterDropModel by remember {
 mutableStateOf(WaterDropModel.waterDropM)
}
 val color1 = colorResource(id = waterDropModel.water1.colorResource)
 val color2 = colorResource(id = waterDropModel.water2.colorResource)
 val color3 = colorResource(id = waterDropModel.water3.colorResource)
 val color4 = colorResource(id = waterDropModel.water4.colorResource)
 val color5 = colorResource(id = waterDropModel.water5.colorResource)
 val color6 = colorResource(id = waterDropModel.water6.colorResource)
 val color7 = colorResource(id = waterDropModel.water7.colorResource)
 val color8 = colorResource(id = waterDropModel.water8.colorResource)

 val animAlpha1 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha2 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha3 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha4 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha5 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha6 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha7 = remember { Animatable(0f, Float.VectorConverter) }
 val animAlpha8 = remember { Animatable(0f, Float.VectorConverter) }


LaunchedEffect(Unit){
animAlpha1.animateTo(1f, animationSpec = myKeyframs(0))
}
LaunchedEffect(Unit){
animAlpha2.animateTo(1f, animationSpec = myKeyframs(1))
}
LaunchedEffect(Unit){
animAlpha3.animateTo(1f, animationSpec = myKeyframs(2))
}
LaunchedEffect(Unit){
animAlpha4.animateTo(1f, animationSpec = myKeyframs(3))
}
LaunchedEffect(Unit){
animAlpha5.animateTo(1f, animationSpec = myKeyframs(4))
}
LaunchedEffect(Unit){
animAlpha6.animateTo(1f, animationSpec = myKeyframs(5))
}
LaunchedEffect(Unit){
animAlpha7.animateTo(1f, animationSpec = myKeyframs(6))
}
LaunchedEffect(Unit){
animAlpha8.animateTo(1f, animationSpec = myKeyframs(7))
}

 Canvas(modifier = Modifier.fillMaxSize()){
 val contentWidth = size.width
 val contentHeight = size.height
 withTransform({
 translate(left = contentWidth / 2, top = contentHeight / 2)}) {
 drawPath(AndroidPath(waterDropModel.water8Path), color = color8, alpha = animAlpha8.value)
 drawPath(AndroidPath(waterDropModel.water7Path), color = color7, alpha = animAlpha7.value)
 drawPath(AndroidPath(waterDropModel.water6Path), color = color6, alpha = animAlpha6.value)
 drawPath(AndroidPath(waterDropModel.water5Path), color = color5, alpha = animAlpha5.value)
 drawPath(AndroidPath(waterDropModel.water4Path), color = color4, alpha = animAlpha4.value)
 drawPath(AndroidPath(waterDropModel.water3Path), color = color3, alpha = animAlpha3.value)
 drawPath(AndroidPath(waterDropModel.water2Path), color = color2, alpha = animAlpha2.value)
 drawPath(AndroidPath(waterDropModel.water1Path), color = color1, alpha = animAlpha1.value)
}
}
}

private fun myKeyframs(num:Int):KeyframesSpec<Float>{
return keyframes{
durationMillis = 3000
delayMillis = num * 2000
0.5f at 1000 with LinearEasing
0.2f at 2000 with LinearEasing
}
}

然后就是外层扫光的动画,像探照灯一样一圈圈的扫,一共扫7遍,代码跟层变动画差不多,也是对alpha值做渐变,目前代码是调用扫光动画7次,后续看看如何优化性能。每次调用传入不同的delay值即可。


@Composable
fun drawWaterDropScan(delayTime:Long){
   val waterDropModel by remember {
       mutableStateOf(WaterDropModel.waterDropMScan)
  }
   val color1 = colorResource(id = waterDropModel.water1.colorResource)
   val color2 = colorResource(id = waterDropModel.water2.colorResource)
   val color3 = colorResource(id = waterDropModel.water3.colorResource)
   val color4 = colorResource(id = waterDropModel.water4.colorResource)
   val color5 = colorResource(id = waterDropModel.water5.colorResource)
   val color6 = colorResource(id = waterDropModel.water6.colorResource)
   val color7 = colorResource(id = waterDropModel.water7.colorResource)
   val color8 = colorResource(id = waterDropModel.water8.colorResource)
   val animAlpha2 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha3 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha4 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha5 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha6 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha7 = remember { Animatable(0f, Float.VectorConverter) }
   val animAlpha8 = remember { Animatable(0f, Float.VectorConverter) }

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(1f, 350) }
animAlpha2.animateTo(0f, animationSpec = myKeyframs2(700, 0, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.8f, 315) }
animAlpha3.animateTo(0f, animationSpec = myKeyframs2(630, 233, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.55f, 315) }
animAlpha4.animateTo(0f, animationSpec = myKeyframs2(630, 383, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.5f, 325) }
animAlpha5.animateTo(0f, animationSpec = myKeyframs2(650, 533, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.45f, 325) }
animAlpha6.animateTo(0f, animationSpec = myKeyframs2(650, 667, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.35f, 283) }
animAlpha7.animateTo(0f, animationSpec = myKeyframs2(567, 816, map))
}

LaunchedEffect(Unit){
delay(delayTime)
val map = mutableMapOf<Float, Int>().apply { put(0.3f, 216) }
animAlpha8.animateTo(0f, animationSpec = myKeyframs2(433, 983, map))
}

 Canvas(modifier = Modifier.fillMaxSize()){
 val contentWidth = size.width
 val contentHeight = size.height

 withTransform({
 translate(left = contentWidth / 2, top = contentHeight / 2)
}) {
 drawPath(AndroidPath(waterDropModel.water8Path), color = color8, alpha = animAlpha8.value)
 drawPath(AndroidPath(waterDropModel.water7Path), color = color7, alpha = animAlpha7.value)
 drawPath(AndroidPath(waterDropModel.water6Path), color = color6, alpha = animAlpha6.value)
 drawPath(AndroidPath(waterDropModel.water5Path), color = color5, alpha = animAlpha5.value)
 drawPath(AndroidPath(waterDropModel.water4Path), color = color4, alpha = animAlpha4.value)
 drawPath(AndroidPath(waterDropModel.water3Path), color = color3, alpha = animAlpha3.value)
 drawPath(AndroidPath(waterDropModel.water2Path), color = color2, alpha = animAlpha2.value)
 drawPath(AndroidPath(waterDropModel.water1Path), color = color1)
}
}
}

private fun myKeyframs2(durationMillisParams:Int, delayMillisParams:Int, frames:Map<Float, Int>):KeyframesSpec<Float>{
return keyframes{
durationMillis = durationMillisParams
delayMillis = delayMillisParams
for ((valueF, timestamp) in frames){
valueF at timestamp
}
}
}


@Preview
@Composable
fun WaterDrop(){
   Box(modifier = Modifier.fillMaxSize()){
       drawWaterDropBg()
       drawWaterDrop()
       for (num in 1 .. 7){
           drawWaterDropScan(delayTime = num * 2000L)
      }
  }
}

代码跟LocationMarker在一个Project里面,暂时没有添加导航。github.com/yinxiucheng… 下的CustomerComposeView.


作者:cxy107750
链接:https://juejin.cn/post/7198965240279679035
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android约束布局ConstraintLayout的使用

Android引入约束布局的目的是为了减少布局层级的嵌套,从而提升渲染性能。约束布局综合线性布局、相对布局、帧布局的部分功能,缺点也很明显,就是可能要多写几行代码。所以约束布局使用时,还得综合考虑代码量。提升性能也并不一定非得使用约束布局,也可以在ViewGr...
继续阅读 »

Android引入约束布局的目的是为了减少布局层级的嵌套,从而提升渲染性能。约束布局综合线性布局、相对布局、帧布局的部分功能,缺点也很明显,就是可能要多写几行代码。所以约束布局使用时,还得综合考虑代码量。提升性能也并不一定非得使用约束布局,也可以在ViewGroup上dispatchDraw。你需要根据业务的具体情况选择最合适的实现方式。我知道很多人一开始很不习惯使用约束布局,但既然你诚心诚意问我怎么使用了?于是我就大发慈悲告诉你怎么使用呗。


链式约束


用得最多的非链式约束莫属了。这看起来是不是类似于相对布局?那么有人问了,既然相对布局写法这么简洁,都不用强制你写另一个方向的占满屏幕的约束,为什么还要使用约束布局呢?约束布局它还是有它过布局之处的,比如以下一些功能,相对布局是做不到的。


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#F0F"
app:layout_constraintVertical_chainStyle="spread"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/iv2"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0"
app:layout_constraintBottom_toTopOf="@id/iv3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv1"/>
<ImageView
android:id="@+id/iv3"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0FF"
app:layout_constraintBottom_toTopOf="@id/iv4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv2"/>
<ImageView
android:id="@+id/iv4"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0F0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv3"/>
</androidx.constraintlayout.widget.ConstraintLayout>

我们可以在链首的控件添加一个layout_constraintVertical_chainStyle属性为spread,翻译成展开,在我看来就是排队,要保持间距一样,而且边缘不能站,默认不写也是指定的spread。


链式约束spread
如果你改成spread_inside,就会变成可以靠墙的情况。


链式约束spread inside
那如果你改成packed,就会贴在一起了。


链式约束packed


使用Group分组进行显示和隐藏


而如果你添加以下代码在布局中,就会将id为iv1和iv3点色块去掉,这样iv2和iv4就贴在一起了。


<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="iv1,iv3" />

分组约束


Guideline引导线


<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"/>

使用引导线,可以在预览布局的时候看到,在运行时是看不到的,可以作为布局的参考线。


引导线
切换到Design的选项卡你就能看到了。
引导线的另外两个属性是layout_constraintGuide_begin和layout_constraintGuide_end,一看就知道这个是使用边距定位的。


角度约束


角度约束的以下三个属性是一起使用的。


layout_constraintCircle  
layout_constraintCircleAngle
layout_constraintCircleRadius

<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FFC0C0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0"
app:layout_constraintCircle="@id/iv1"
app:layout_constraintCircleAngle="30"
app:layout_constraintCircleRadius="150dp"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"/>

</androidx.constraintlayout.widget.ConstraintLayout>

知道你们喜欢粉嫩的,所以特地把色块的颜色换了一下。旋转角是以垂直向上为0度角,顺时针旋转30度。距离则是计算两控件重心的连线。在矩形区域中,重心就在对角线的交叉点。


角度约束


位置百分比偏移


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FFC0C0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/iv2"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv1"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"/>
</androidx.constraintlayout.widget.ConstraintLayout>

这里需要注意,不是只使用layout_constraintHorizontal_bias就可以了,原有该方向的约束也不能少。


百分比偏移


使用goneMargin设置被依赖的控件gone时,依赖控件的边距


goneMargin有以下属性:


layout_goneMarginStart  
layout_goneMarginEnd
layout_goneMarginTop
layout_goneMarginBottom

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/iv1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FFC0C0"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/iv2"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0"
app:layout_goneMarginTop="10dp"
app:layout_constraintTop_toBottomOf="@+id/iv1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="10dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

goneMargin


约束宽高比


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv1"
android:layout_width="0dp"
android:layout_height="100dp"
app:layout_constraintDimensionRatio="1:1"
android:background="#FFC0C0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/iv2"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="0dp"
android:background="#FF0"
app:layout_constraintDimensionRatio="H,3:2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv1"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

我们可以将宽高的其中一个设置为0dp,然后这个宽或高根据相对于另一个的比例来。如果高度为0dp,需要根据宽度来确认高度,你可以直接赋值为3:2,也可以赋值为H,3:2,这也是推荐的写法,我一般省略W和H。如果高度为0dp,你本应该写H,而你写成了W,那就要把比例反过来看宽高比。


约束宽高比


权重约束


这个类似于线性布局的权重功能。


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv1"
android:layout_width="100dp"
android:layout_height="0dp"
app:layout_constraintVertical_weight="1"
android:background="#FFC0C0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/iv2"/>

<ImageView
android:id="@+id/iv2"
android:layout_width="100dp"
android:layout_height="0dp"
app:layout_constraintVertical_weight="2"
android:background="#FF0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv1"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

权重约束


作者:dora
链接:https://juejin.cn/post/7199297355748409399
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

兔兔进度条——安卓WebView添加进度条

前言 本来准备过年时候慢慢写,但过完年才发现一篇都没写,真是难为情,今天我们就看看RabbitAPP中进入webview中使用的兔兔进度条,参考了51博客网的自定义progress的方法(自定义view之自定义电池效果_xiyangyang8110的技术博客_...
继续阅读 »

前言


本来准备过年时候慢慢写,但过完年才发现一篇都没写,真是难为情,今天我们就看看RabbitAPP中进入webview中使用的兔兔进度条,参考了51博客网的自定义progress的方法(自定义view之自定义电池效果_xiyangyang8110的技术博客_51CTO博客),其实还是挺简陋的,本来想画一个兔子跑的指示器的progress的,但是想了半天没动手,还是采用这种最简单的方法。


正篇


最终效果


首先我们来看看效果:


c5073ddb04cb10cfced2b237e4781e44.gif


由于网络非常好,所以加载速度也很快,我们可以看到兔子背景逐渐被红色覆盖。

实现方法


实现方法其实很简单,先给一张图片,然后调用ProgressBar控件覆盖它,并且重新写ProgressBar的样式:


image.png


<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="StyleRabbitProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:progressDrawable">@drawable/shape_progressbar</item>
</style>
</resources>

我们这里使用了ProgressBar的水平进度条样式,然后对其sprogressDrawable重新添加shape:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--progressbar的背景颜色-->
<!-- <item android:id="@android:id/background">-->
<!-- <shape>-->
<!-- <corners android:radius="5dip" />-->
<!-- <gradient-->
<!-- android:startColor="@color/black"-->
<!-- android:centerColor="@color/blue"-->
<!-- android:endColor="@color/black"-->
<!-- android:angle="270"-->
<!-- />-->
<!-- </shape>-->
<!-- </item>-->
<!--progressBar的缓冲进度颜色-->
<!-- <item android:id="@android:id/secondaryProgress">-->
<!-- <clip>-->
<!-- <shape>-->
<!-- <corners android:radius="5dip" />-->
<!-- <gradient-->
<!-- android:startColor="@color/white"-->
<!-- android:centerColor="@color/white"-->
<!-- android:endColor="@color/white"-->
<!-- android:angle="270"-->
<!-- />-->
<!-- </shape>-->
<!-- </clip>-->
<!-- </item>-->
<!--progressBar的最终进度颜色-->
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#33E91E63"
android:centerColor="#33E91E63"
android:endColor="#33E91E63"
android:angle="270"
/>
</shape>
</clip>
</item>
</layer-list>

根据需要对进度颜色进行控制,我们最终采用棕红色,对进度条颜色变更,最后我们加入到webview页面的布局中:


<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/rabbit_progress" />
<ProgressBar
android:id="@+id/progressRabbit"
android:layout_marginTop="5dp"
android:layout_marginStart="4dp"
style="@style/StyleRabbitProgressBar"
android:layout_width="130dp"
android:layout_height="120dp"
android:max="100" />
</RelativeLayout>

最后,再到webview页面的Activity代码中控制显示:


 binding.vWebView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
Log.i("onPageStarted", "页面加载")

binding.progressRabbit.progress = newProgress
}

我们通过WebView的webChromeClient方法对onProgressChanged复写,对其中的newProgress参数赋值给我们进度条控件的progress参数,这样就起到了对网页加载的可视化。


于是我们就可以在web加载的时候看到上面有个兔子,兔子的背景全红后就加载好网页了。


总结


这个进度条现在越看越难受,下一次会把进度条重新制作一遍,还是把它作为指示器去绘制一个进度条比较好,不过之前我写自定义view都是用Java,Kotlin中还是不会写,希望能尽快学会用Kotlin自定义view,感谢您的观看。


作者:ObliviateOnline
链接:https://juejin.cn/post/7197025946929627195
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

兔兔进度条Plus——SeekBar充当Progress

前言 之前写的progress其实根本没有起到进度条的作用,太显眼,而且并不好看,所以有了新的想法,我们将ProgressBar控件换成SeekBar控件,然后再将它的thumb指示器换成小兔子即可。 正篇 实现过程 首先,我们在需要进度条的页面布局的最开始加...
继续阅读 »

前言


之前写的progress其实根本没有起到进度条的作用,太显眼,而且并不好看,所以有了新的想法,我们将ProgressBar控件换成SeekBar控件,然后再将它的thumb指示器换成小兔子即可。


正篇


实现过程


首先,我们在需要进度条的页面布局的最开始加上下面代码:


<SeekBar
android:id="@+id/vSeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:thumb="@mipmap/rabbit_progress"/>

其中thumb属性就是可以让你换指针样式的,而SeekBar其实也是多用于播放器的进度选择器之类的,由于seekbar是可以拖动的,所以我们得把控件拖动给禁止了:


binding.vSeekBar.isEnabled = false

接着,我们为了更好的展示效果,在seekbar控件下面加了一个Button:


image.png


binding.vButton.setOnClickListener {
if (binding.vSeekBar.visibility != View.GONE) {
binding.vSeekBar.progress += 10
}
if (binding.vSeekBar.progress == 100) {
binding.vSeekBar.progress = 0
}
}

添加完按钮后,我们为按钮设置点击事件,每点一次就会出现进度条加10的事件,到达100后再置为0重复操作:


f6e01d316d1532e92a789f5e2291e923.gif


这样,我们就有了一个兔子往前进的进度条,然后我们再把Button去除,再换到webview的webChromeClient中的重写方法onProgressChanged中控制进度条增加的逻辑即可:
```Kotlin
binding.vSeekBar.progress = newProgress
if (newProgress == 100) {
binding.vSeekBar.visibility = View.GONE
} else {
binding.vSeekBar.visibility = View.VISIBLE
}
```
当进度条加完后,就隐藏该控件,这样也就不会一直看到这个控件。
# 总结
虽然内容不多,但是问题还是很多的,如果可以再把style样式做一下,效果会更好,然后再重新定义一下进度条本体的颜色和形状,不过,目前我对这部分还看的比较少,网上看到的自定义也非常繁多,等后面用Kotlin自定义View熟练了再重新画一个Progress或SeekBar.

作者:ObliviateOnline
链接:https://juejin.cn/post/7197753883422392378
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

DialogX 的一些骚包的高阶使用技巧

DialogX 的一些骚包的高阶使用技巧DialogX 是一款轻松易用的对话框组件,具备高扩展性和易上手的特点,包含各种自定义主题样式,可以快速实现各式各样的对话框效果,也避免了 AlertDialog 的诸多蛋疼的问题,详情可以参阅这篇文章:《使用 Dial...
继续阅读 »

DialogX 的一些骚包的高阶使用技巧

DialogX 是一款轻松易用的对话框组件,具备高扩展性和易上手的特点,包含各种自定义主题样式,可以快速实现各式各样的对话框效果,也避免了 AlertDialog 的诸多蛋疼的问题,详情可以参阅这篇文章:《使用 DialogX 快速构建 Android App 对话框》

本篇文章将介绍一些 DialogX 的使用技巧,也欢迎大家集思广益在评论区留下宝贵的建议,DialogX 自始至终的目标都是尽量让开发变得更加简单,基于此目的,DialogX 首先想做的就是避免重复性劳动,一般我们开发产品总会有一些各式各样的需要,比如关于对话框启动和关闭的动画。

局部>组件内>全局生效的属性

局部设置

DialogX 的很多属性都可以自定义调整,最简单的就是通过实例的 set 方法对属性进行调整,例如对于动画,你可以使用这些 set 方法进行调整:


但是,当我们的程序中有大量的对话框,但每个 MessageDialog 都需要调整,又不能影响其他对话框的动画,该怎么设置呢?

组件生效

此时就可以使用该对话框的静态方法直接进行设置,例如:

MessageDialog.overrideEnterDuration = 100;    //入场动画时长为100毫秒
MessageDialog.overrideExitDuration = 100;     //出场动画时长为100毫秒
MessageDialog.overrideEnterAnimRes = R.anim.anim_dialogx_top_enter; //入场动画资源
MessageDialog.overrideExitAnimRes = R.anim.anim_dialogx_top_exit;   //出场动画资源

如果要设置的属性想针对全局,也就是所有对话框都生效,此时可以使用全局设置进行调整:

全局设置

你可以随时召唤神龙 DialogX,直接修改静态属性,这里的设置都是针对全局的,可以快速完成需要的调整。

DialogX.enterAnimDuration = 100;
DialogX.exitAnimDuration = 100;

上边演示的是动画相关设置,除此之外,你还可以对对话框的标题文字样式、对话框OK按钮的样式、取消按钮的样式、正文内容的文字样式等等进行全局的调整,只需要知道属性生效的优先级是:

优先级为:实例使用set方法设置 > 组件override设置 > 全局设置。

额外的,如果需要对部分组件的行为进行调整,例如 PopTip 的默认显示位置位于屏幕底部,但产品或设计要求想显示到屏幕中央,但这个设置又取决于主题的限制,此时你可以通过重写主题的设置来实现调整:

覆盖主题设置

想要将 PopTip 吐司提示不按照主题的设定(例如屏幕底部)显示,而是以自己的要求显示(例如屏幕中央),但对于 PopTip 的 align 属性属于主题控制的,此时可以通过重写主题来调整对话框的部分行为,例如:

DialogX.globalStyle = new MaterialStyle(){
   @Override
   public PopTipSettings popTipSettings() {
       return new PopTipSettings() {
           @Override
           public ALIGN align() {
               return ALIGN.CENTER;
          }
      };
  }
};

DialogX 强大的扩展性允许你发挥更多想象空间!如果你的产品经理或者设计师依然不满足于简简单单的动画,想要定制更为丰富的入场/出场效果,此时可以利用 DialogX 预留的对话框动画控制接口对每一个对话框内的组件动画细节进行定制。

完全的动画细节定制

例如,我们可以针对一个对话框的背景遮罩进行透明度动画效果处理,但对于对话框内容部分进行一个从屏幕顶部进入的动画效果,其他的,请发挥你的想象进行设计吧!

使用 DialogXAnimInterface 接口可以完全自定义开启、关闭动画。

由于 DialogX 对话框组件的内部元素都是暴露的,你可以轻松获取并访问内部实例,利用这一点,再加上 DialogXAnimInterface 会负责对话框启动和关闭的动画行为,你可以充分利用它实现你想要的效果。

例如对于一个 CustomDialog,你可以这样控制其启动和关闭动画:

CustomDialog.show(new OnBindView<CustomDialog>(R.layout.layout_custom_dialog) {
           @Override
           public void onBind(final CustomDialog dialog, View v) {
               //...
          }
      })
       //实现完全自定义动画效果
      .setDialogXAnimImpl(new DialogXAnimInterface<CustomDialog>() {
           //启动对话框动画逻辑
           @Override
           public void doShowAnim(CustomDialog customDialog, ObjectRunnable<Float> animProgress) {
               //创建一个资源动画
               Animation enterAnim;
               int enterAnimResId = com.kongzue.dialogx.R.anim.anim_dialogx_top_enter;
               enterAnim = AnimationUtils.loadAnimation(me, enterAnimResId);
               enterAnim.setInterpolator(new DecelerateInterpolator(2f));
               long enterAnimDurationTemp = enterAnim.getDuration();
               enterAnim.setDuration(enterAnimDurationTemp);
               customDialog.getDialogImpl().boxCustom.startAnimation(enterAnim); //通过 getDialogImpl() 获取内部暴露的 boxCustom 元素
               //创建一个背景遮罩层的渐变动画
               ValueAnimator bkgAlpha = ValueAnimator.ofFloat(0f, 1f);
               bkgAlpha.setDuration(enterAnimDurationTemp);
               bkgAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                   @Override
                   public void onAnimationUpdate(ValueAnimator animation) {
                       //汇报动画进度,同时 animation.getAnimatedValue() 将改变遮罩层的透明度
                       animProgress.run((Float) animation.getAnimatedValue());
                  }
              });
               bkgAlpha.start();
          }
           
           //关闭对话框动画逻辑
           @Override
           public void doExitAnim(CustomDialog customDialog, ObjectRunnable<Float> animProgress) {
               //创建一个资源动画
               int exitAnimResIdTemp = com.kongzue.dialogx.R.anim.anim_dialogx_default_exit;
               Animation exitAnim = AnimationUtils.loadAnimation(me, exitAnimResIdTemp);
               customDialog.getDialogImpl().boxCustom.startAnimation(exitAnim); //通过 getDialogImpl() 获取内部暴露的 boxCustom 元素
               //创建一个背景遮罩层的渐变动画
               ValueAnimator bkgAlpha = ValueAnimator.ofFloat(1f, 0f);
               bkgAlpha.setDuration(exitAnim.getDuration());
               bkgAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                   @Override
                   public void onAnimationUpdate(ValueAnimator animation) {
                       //汇报动画进度,同时 animation.getAnimatedValue() 将改变遮罩层的透明度
                       animProgress.run((Float) animation.getAnimatedValue());
                  }
              });
               bkgAlpha.start();
          }
      });

对于 animProgress 它本质上是个反向回调执行器,因为动画时长不定,你需要通知 DialogX 当前你的动画到达哪个阶段了,对话框需要根据这个阶段进行操作处理,例如关闭动画执行过程应当是 1f 至 0f 的过程,完毕后应当销毁对话框,那么当 animProgress.run(0f) 时就会执行销毁流程,而启动动画应当是 0f 至 1f 的过程,当 animProgress.run(1f) 时启动对话框的动画完全执行完毕。

另外,你有没有注意到上述代码中的一个小细节?你可以通过 .getDialogImpl() 访问对话框的所有内部实例,这意味着,DialogX 中的所有实例事实上都是对外开放的,你可以在对话框启动后(DialogLifecycle#onShow)通过 DialogImpl 获取对话框的所有内容组件,对他们进行你想做的调整和设置,这都将极大程度上方便开发者对对话框内容进行定制。

正如我一开始所说,DialogX 将坚持努力打造一款更好用,更高效可定制化的对话框组件。

队列对话框

某些场景下需要有“模态”对话框的需要,即,一次性创建多个对话框,组成队列,逐一显示,当上一个对话框关闭时自动启动下一个对话框,此时可以使用队列对话框来完成。

示例代码如下,在 DialogX.showDialogList(...) 中构建多个对话框,请注意这些对话框必须是没有启动的状态,使用 .build() 方法完成构建,以 “,” 分隔组成队列,即可自动启动。

DialogX.showDialogList(
       MessageDialog.build().setTitle("提示").setMessage("这是一组消息对话框队列").setOkButton("开始").setCancelButton("取消")
              .setCancelButton(new OnDialogButtonClickListener<MessageDialog>() {
                   @Override
                   public boolean onClick(MessageDialog dialog, View v) {
                       dialog.cleanDialogList();
                       return false;
                  }
              }),
       PopTip.build().setMessage("每个对话框会依次显示"),
       PopNotification.build().setTitle("通知提示").setMessage("直到上一个对话框消失"),
       InputDialog.build().setTitle("请注意").setMessage("你必须使用 .build() 方法构建,并保证不要自己执行 .show() 方法").setInputText("输入文字").setOkButton("知道了"),
       TipDialog.build().setMessageContent("准备结束...").setTipType(WaitDialog.TYPE.SUCCESS),
       BottomDialog.build().setTitle("结束").setMessage("下滑以结束旅程,祝你编码愉快!").setCustomView(new OnBindView<BottomDialog>(R.layout.layout_custom_dialog) {
           @Override
           public void onBind(BottomDialog dialog, View v) {
               ImageView btnOk;
               btnOk = v.findViewById(R.id.btn_ok);
               btnOk.setOnClickListener(new View.OnClickListener() {
                   @Override
                   public void onClick(View v) {
                                       dialog.dismiss();
                                  }
              });
          }
      })
);

使用过程中,随时可以使用 .cleanDialogList() 来停止接下来的队列对话框的显示。

尾巴

DialogX 正在努力打造一款对开发者更友好,使用起来更为简单方便的对话框组件,若你有好的想法,也欢迎加入进来一起为 DialogX 添砖加瓦,通过 Github 一起让 DialogX 变得更加强大!

DialogX 路牌:github.com/kongzue/Dia…

作者:Kongzue
来源:juejin.cn/post/7197687219581993021

收起阅读 »

疫情过后的这个春招,真的会回暖吗?

今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪个景点人满为患到走不动路的时候,都...
继续阅读 »

今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。

这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪个景点人满为患到走不动路的时候,都觉得自己宅在家里哪也不去真的是太对了。

好了回归正题,很多小伙伴们非常关注的一个问题,在经历了疫情放开、大规模感染的相对平稳后,这一届春招真的会回暖吗?

在聊春招之前,我觉得还是有必要再来科普一下春招的时间线。

  • 12月,一般只有少量的企业开始进行春招提前批预热,或是进行秋招的补录

  • 1月,部分公司开启春招正式批

  • 3-4月,才是春招的高峰期,大部分公司在这个时间段陆续开启春招

  • 5月,大部分的企业会结束招聘

为了了解今年的形势,我也逛了不少论坛,了解到有一些大厂在去年12月底的时候,就已经开始了秋招的补录,不少人收到了补录的通知。


通过整体氛围来看,今年春招大概率会比去年进行一波升温,在岗位的可选择性上,大伙可能也有更多的大厂岗位可以进行一波冲击。尽管如此我还是劝大家要尽早准备,因为虽然说是春招,但并不是真正到了春天才真正开始,并且春招的难度比秋招可能还要高上不少。

首先,相对于秋招来说,春招的岗位会少很多,因为春招更多是对于秋招的补充,是一个查漏补缺的过程,对秋招中没有招满、或者有新岗位出现的情况下,才会在春招中放出该岗位。少量的岗位,需要你能更好的把握信息资源,迅速出击。

其次,你可能拥有更多的竞争对手,考研、考公失利的同学如果不选择二战,将会大量涌入春招,而对于秋招找到的工作不满意想要跳槽的同学,有笔试面试经验、工作经历,将会成为你春招路上麻烦的对手。

所以说到底,大家还是不要过于盲目乐观,扎扎实实的准备肯定是不能少的,毕竟春招的难度摆在这里。在看到大规模补录的同时,我们也不能否认背后的裁员依旧存在。有可能你现在看到的hc,就是在不久前刚刚通过裁员所释放的。

另外,我还是得说点泼冷水的话,虽然看上去形势一片大好,岗位放开了很多,但不代表薪资待遇还是和以前一样的,从一个帖子中可以看到,即便是在杭州和成都的中厂里,降薪也是存在的。


因为说到底,疫情并不是经济下行的根本原因,想要寄希望于疫情放开后经济能够快速复苏基本是不可能的。

国内的互联网公司,已经过了那个爆发式发展的黄金时期,甚至说一句互联网公司规模已经能隐隐约约窥到顶峰也不过分。美联储加息、中概股暴跌、企业融资困难…面对这些困难的环境,即使疫情放开也于事无补。

尽管环境如此困难,我仍然认为互联网行业是小镇做题家们快速实现社会价值、积累财富的黄金职业。看看大厂里十几万、几十万的年终奖,并不是每个行业都能做到的。

最后还是建议大家,积极准备,不管这个春招是否回暖,还是要做到尽量不留遗憾,不要给自己找借口,再寄希望于下一个秋招。

2023年,我们一起加油!

作者:码农参上
来源:juejin.cn/post/7193885908129546277

收起阅读 »

5分钟带你了解Android Progress Bar

1、前言 最近在开发中,同事对于android.widget下的控件一知半解,又恰好那天用到了Seekbar,想了想,那就从Seekbar's father ProgressBar 来说说android.widget下的常用控件和常用用法吧。后面也会根据这些控...
继续阅读 »

1、前言


最近在开发中,同事对于android.widget下的控件一知半解,又恰好那天用到了Seekbar,想了想,那就从Seekbar's father ProgressBar 来说说android.widget下的常用控件和常用用法吧。后面也会根据这些控件来进行仿写、扩展,做一些高度自定义的View啦。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏


2、ProgressBar


A user interface element that indicates the progress of an operation.


使用很简单,看看一些基本的属性


android:max:进度条的最大值
android:progress:进度条已完成进度值
android:progressDrawable:设置轨道对应的Drawable对象
android:indeterminate:如果设置成true,则进度条不精确显示进度(会一直进行动画)
android:indeterminateDrawable:设置不显示进度的进度条的Drawable对象
android:indeterminateDuration:设置不精确显示进度的持续时间
android:secondaryProgress:二级进度条(使用场景不多)
复制代码

直接在布局中使用即可


        <ProgressBar
style="@android:style/Widget.ProgressBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />

<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />

<ProgressBar
style="@android:style/Widget.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />

<ProgressBar
android:id="@+id/sb_no_beautiful"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:max="100"
android:progress="50"
android:secondaryProgress="70" />

<ProgressBar
android:id="@+id/sb_no_beautiful2"
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:indeterminate="true"
android:max="100"
android:progress="50"
android:secondaryProgress="70" />
复制代码

分别就对应以下图片咯


image-20230206162049591

但是这种样式,不得不怀疑Google之前的审美,肯定是不满意的,怎么换样式呢。


看看XML文件,很容易发现,这几个ProgressBar的差异是因为style引起的,随手点开一个@android:style/Widget.ProgressBar.Horizontal 看看。


    <style name="Widget.ProgressBar.Horizontal">
<item name="indeterminateOnly">false</item>
<item name="progressDrawable">@drawable/progress_horizontal</item>
<item name="indeterminateDrawable">@drawable/progress_indeterminate_horizontal</item>
<item name="minHeight">20dip</item>
<item name="maxHeight">20dip</item>
<item name="mirrorForRtl">true</item>
</style>
复制代码

很好,估摸着样式就出在progressDrawable/indeterminateDrawable上面,看看 @drawable/progress_horizontal 里面


<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#ff9d9e9d"
android:centerColor="#ff5a5d5a"
android:centerY="0.75"
android:endColor="#ff747674"
android:angle="270"/>
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#80ffd300"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#a0ffcb00"
android:angle="270"/>
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#ffffd300"
android:centerColor="#ffffb600"
android:centerY="0.75"
android:endColor="#ffffcb00"
android:angle="270"/>
</shape>
</clip>
</item>
</layer-list>


复制代码

一个样式文件,分别操控了background/secondaryProgress/progress,这样我们很容易推测出


image-20230206112729207

再看看 @drawable/progress_indeterminate_horizontal


<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
<item android:drawable="@drawable/progressbar_indeterminate1" android:duration="200" />
<item android:drawable="@drawable/progressbar_indeterminate2" android:duration="200" />
<item android:drawable="@drawable/progressbar_indeterminate3" android:duration="200" />
</animation-list>
复制代码

显而易见,这是indeterminate模式下的样式啊,那我们仿写一个不同样式,就很简单了,动手。



styles.xml



<style name="ProgressBar_Beautiful" >
<item name="android:indeterminateOnly">false</item>
<item name="android:progressDrawable">@drawable/progress_horizontal_1</item>
<item name="android:indeterminateDrawable">@drawable/progress_indeterminate_beautiful</item>
<item name="android:mirrorForRtl">true</item>
</style>
复制代码


progress_horizontal_1.xml



<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="25dp" />
<solid android:color="#FFF0F0F0"/>
</shape>
</item>

<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="25dp" />
<solid android:color="#FFC0EC87"/>

</shape>
</clip>
</item>

<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="25dp" />
<solid android:color="#FFA5E05B"/>
</shape>
</clip>
</item>
</layer-list>
复制代码


progress_indeterminate_beautiful.xml



<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/bg_progress_001" android:duration="200" />
<item android:drawable="@drawable/bg_progress_002" android:duration="200" />
<item android:drawable="@drawable/bg_progress_003" android:duration="200" />
<item android:drawable="@drawable/bg_progress_004" android:duration="200" />
</animation-list>
复制代码

吭呲吭呲就写出来了,看看效果


2023-02-06_16-24-14 (2)


换了个颜色,加了个圆角/ 换了个图片,还行。


我没有去再写环形的ProgressBar了,因为它就是个一个图,疯狂的在旋转。


<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/spinner_white_76"
android:pivotX="50%"
android:pivotY="50%"
android:framesCount="12"
android:frameDuration="100" />
复制代码

还有一些属性我就不赘述了。你可以根据官方的样式,修一修、改一改,就可以满足一些基本的需求了。


用起来就这么简单,就是因为太简单,更复杂的功能就不是ProgressBar能直接实现的了。比如带个滑块?


3、SeekBar


好吧,ProgressBar的一个子类,也在android.widget下,因为是直接继承,而且就加了个滑块相关的代码,实际上它也非常简单,然我们来看看


<SeekBar
android:id="@+id/sb_01"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:thumbOffset="1dp"
android:max="100"
android:progress="50"
android:secondaryProgress="70"
android:splitTrack="false"
android:thumb="@drawable/icon_seekbar_thum" />

<SeekBar
android:id="@+id/sb_02"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:max="100"
android:progress="50"
android:secondaryProgress="70"
android:thumb="@drawable/icon_seekbar_thum" />

<SeekBar
android:id="@+id/sb_03"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:max="100"
android:progress="100"
android:secondaryProgress="70"
android:splitTrack="false"
android:thumb="@drawable/icon_seekbar_thum" />

<SeekBar
android:id="@+id/sb_04"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:thumbOffset="1dp"
android:max="100"
android:progress="100"
android:secondaryProgress="70"
android:splitTrack="false"
android:thumb="@drawable/icon_seekbar_thum" />

<SeekBar
android:id="@+id/sb_05"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:max="100"
android:paddingHorizontal="0dp"
android:progress="50"
android:secondaryProgress="70"
android:thumb="@drawable/icon_seekbar_thum" />


<SeekBar
android:id="@+id/sb_06"
style="@style/ProgressBar_Beautiful"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginVertical="10dp"
android:max="100"
android:progress="50"
android:secondaryProgress="70"
android:thumb="@null" />
复制代码

样式就在下面了



因为Seekbar相较而言就多了个thumb(就是那个滑块),所以就着重说一下滑块,其他的就一笔带过咯。


主要了解的是如何设置自己的thumb和thumb的各种问题


android:thumb="@drawable/icon_seekbar_thum"
复制代码

设置就这么thumb简单,一个drawable文件解决,我这里对应的是单一图片,不过Google的是带有多种状态的thumb,我们来看看官方是如何实现的


<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize="true">
<item android:state_enabled="false" android:state_pressed="true">
<bitmap android:src="@drawable/abc_scrubber_control_off_mtrl_alpha"android:gravity="center"/>
</item>
<item android:state_enabled="false">
<bitmap android:src="@drawable/abc_scrubber_control_off_mtrl_alpha"android:gravity="center"/>
</item>
<item android:state_pressed="true">
<bitmap android:src="@drawable/abc_scrubber_control_to_pressed_mtrl_005" android:gravity="center"/>
</item>
<item>
<bitmap android:src="@drawable/abc_scrubber_control_to_pressed_mtrl_000"android:gravity="center"/>
</item>
</selector>
复制代码

引用一个drawable,也是一个熟知的selector组,通过对应的item,我们就可以实现在不同的状态下显示不同的thumb了,具体的样式我就不写了,再说ProgressBar的样式的时候也是有类似的操作的


不过你可能发现了,其实这几个样式看起来都差不多,是因为都是我使用Seekbar遇到的问题以及解决方法,我们细说


(1) 自定义的thumb的背景会裁剪出一个正方形,这对于不规则图形来讲是非常难看的



很简单一行



android:splitTrack="false"



修复0。0


(2)thumb的中心点对齐bar的边界,所以thumb是允许超出进度条一点的。有时候我们不需要



很简单一行



android:thumbOffset="1dp"



修复0,0


(3) 你可能发现就算没有写margin和padding,seekbar也不会占满父布局的,是因为它自带padding,所以如果需要去掉



很简单一行



android:paddingHorizontal="0dp"



修复0>0


(4)最后一个,SeekBar但是不想要滑块!为什么不用ProgressBar呢?没别的就是头铁!


很简单一行



android:thumb="@null"



修复0」0


但是要注意的是,此时Seekbar还是能点击的!所以需要把点击事件拦截掉


sb02.setOnTouchListener { _, _ -> true }
复制代码

真的修复0[]0


好了好了,thumb的监听事件还没说呢


            sb01.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
//进度发生改变时会触发
}

override fun onStartTrackingTouch(p0: SeekBar?) {
//按住SeekBar时会触发
}

override fun onStopTrackingTouch(p0: SeekBar?) {
//放开SeekBar时触发
}
})
复制代码

没啦,Seekbar就这么多。


还有一个,放在下次讲吧


对了,如果你感觉你的ProgressBar不够流畅,可以用以下这个


bar.setProgress(progress, true)
复制代码

4、结尾


更多复杂的进度条需求,靠widget的控件,肯定是难以实现的,我们接下来会讲述RatingBar,以及继承ProgressBar,做更多好看的进度条!


没啦,这次就这么多。


作者:AlbertZein
来源:juejin.cn/post/7196994916509286437
收起阅读 »

鸿蒙3.0应用开发若干问题

1.如何去掉默认标题栏,实现全屏显示?在config.json中的ability配置信息中添加属性:2.应用冷启动白屏?这个问题类似与安卓应用冷启动时白屏一样,鸿蒙应用的解决办法同问题1,将主题设置为:注意是Translucent。3.如何获取屏幕尺寸?4.如...
继续阅读 »

在这里插入图片描述

1.如何去掉默认标题栏,实现全屏显示?

在config.json中的ability配置信息中添加属性:

2.应用冷启动白屏?

这个问题类似与安卓应用冷启动时白屏一样,鸿蒙应用的解决办法同问题1,将主题设置为:


注意是Translucent。

3.如何获取屏幕尺寸?

4.如何获取状态栏高度,以及设置状态栏背景色?

5.如何显示Toast提示?



6.网络请求


本文转载自CSDN博客博主白玉梁,原文地址:https://blog.csdn.net/baiyuliang2013/article/details/128236417

收起阅读 »

Builder模式拯救了我的强迫症

前言 Builder模式大家应该不陌生,在我们的编码生涯中,总会碰到它的身影。无论是Android开发中的AlertDialog,还是网络框架中的OkHttp和Retrofit,亦或是JavaPoet中,都有这哥们的身影。 之所以它这么受欢迎,除了它的上手难度...
继续阅读 »

前言


Builder模式大家应该不陌生,在我们的编码生涯中,总会碰到它的身影。无论是Android开发中的AlertDialog,还是网络框架中的OkHttp和Retrofit,亦或是JavaPoet中,都有这哥们的身影。


之所以它这么受欢迎,除了它的上手难度比较低以外,还有一点就是它的的确确的解决了我们日常开发中的一个难题,创建对象时需要的参数过多


举个小例子


过去几年大家都流行炒币,导致市面上一卡难求。随着政府政策的出台,以及虚拟货币的崩盘。显卡不再是有价无市的一种状态。大学刚毕业的小龙开了个电脑店,专门给人配电脑。最开始的时候需求比较简单,只给人记录电脑的CPU,GPU,硬盘等相关信息。


传统的创建对象方式


// 电脑类
class Computer {
private String mBroad;
private String mCPU;
private String mGPU;

public Computer(String broad, String CPU, String GPU) {
mBroad = broad;
mCPU = CPU;
mGPU = GPU;
}

@Override
public String toString() {
return "Computer{" +
", mBroad='" + mBroad + ''' +
", mCPU='" + mCPU + ''' +
", mGPU='" + mGPU + ''' +
'}';
}
}
复制代码

这个时候创建一个Computer对象是这样的:


Computer computer = new Computer("微星 B550M","INTEL I5","NV 3060TI");
复制代码

随着业务量的增大,客户的要求也越来越多。对鼠标,键盘,系统也有了相应的需求。所以Computer类也不得不有了相应的改变。


static class Computer {
private String mOS;
private String mBroad;
private String mKeyBoard;
private String mMouse;
private String mCPU;
private String mGPU;

public Computer(String OS, String broad, String keyBoard, String mouse, String CPU, String GPU) {
mOS = OS;
mBroad = broad;
mKeyBoard = keyBoard;
mMouse = mouse;
mCPU = CPU;
mGPU = GPU;
}

// 就写一个set方法否则文章太长,其他就不写了
public void setmBroad(String mBroad) {
this.mBroad = mBroad;
}

@Override
public String toString() {
return "Computer{" +
"mOS='" + mOS + ''' +
", mBroad='" + mBroad + ''' +
", mKeyBoard='" + mKeyBoard + ''' +
", mMouse='" + mMouse + ''' +
", mCPU='" + mCPU + ''' +
", mGPU='" + mGPU + ''' +
'}';
}
}
复制代码

而创建Computer对象的参数也越来越长:


Computer computer = new Computer("MAC OS","微星 B550M","IQUNIX F97"
,"罗技 MX MASTER3","INTEL I5","NV 3060TI");
复制代码

如果再有新的需求参数,电源,机箱,散热,内存条,硬盘......简直不敢想象。


对象初始化参数问题


此时我们面对的是编程中常见的一个问题,对象中需求的参数过多,而都在构造函数传递,则构造函数就会同例子中一样,太长,要是用set方法来传递,则更为恐怖。


这个时候一个模式就应运而生,他就是建造者模式


建造者模式处理方式


/**
* @author:TianLong
* @date:2022/10/17 19:58
* @detail:产品类
*/
class Computer{
private String mOS;
private String mBroad;
private String mKeyBoard;
private String mMouse;
private String mCPU;
private String mGPU;
private Computer(String OS, String broad, String keyBoard, String mouse, String CPU, String GPU) {
mOS = OS;
mBroad = broad;
mKeyBoard = keyBoard;
mMouse = mouse;
mCPU = CPU;
mGPU = GPU;
}

public static ComputerBuilder createBuilder(){
return new ComputerBuilder();
}

@Override
public String toString() {
return "Computer{" +
"mOS='" + mOS + ''' +
", mBroad='" + mBroad + ''' +
", mKeyBoard='" + mKeyBoard + ''' +
", mMouse='" + mMouse + ''' +
", mCPU='" + mCPU + ''' +
", mGPU='" + mGPU + ''' +
'}';
}

/**
* @author:TianLong
* @date:2022/10/17 19:58
* @detail:产品建造者类
*/
public static class ComputerBuilder{
private String mOS = "Windows";
private String mBroad= "微星 B550M";
private String mKeyBoard= "无";
private String mMouse= "无";
private String mCPU= "Intel I5";
private String mGPU= "AMD 6600XT";

public ComputerBuilder setOS(String OS) {
mOS = OS;
return this;
}

public ComputerBuilder setBroad(String broad) {
mBroad = broad;
return this;
}

public ComputerBuilder setKeyBoard(String keyBoard) {
mKeyBoard = keyBoard;
return this;
}

public ComputerBuilder setMouse(String mouse) {
mMouse = mouse;
return this;
}

public ComputerBuilder setCPU(String CPU) {
mCPU = CPU;
return this;
}

public ComputerBuilder setGPU(String GPU) {
mGPU = GPU;
return this;
}

public Computer build(){
// 可以在build方法中做一些校验等其他工作
if (mBroad.contains("技嘉")){
throw new RuntimeException("技嘉辱华,不支持技嘉主板");
}

Computer computer = new Computer(mOS,mBroad,mKeyBoard,mMouse,mCPU,mGPU);
return computer;
}
}
复制代码

老版本和Builder版本创建对象


// 老版本的Computer对象创建
Computer computer = new Computer("MAC OS","微星 B550M","IQUNIX F97"
,"罗技 MX MASTER3","INTEL I5","NV 3060TI");

// Builder版本的Computer对象创建
Computer computer =Computer.createBuilder()
.setCPU("AMD 5600X")
.setGPU("NV 3060TI")
.setMouse("罗技 MX MASTER3")
.setKeyBoard("IQUNIX F97")
.build();
复制代码

两个版本一对比就能体现出来优势。老版本构造函数中的参数太多太长,同一个类型的参数很容易传错位,经常传参数的时候,还要看看第几个参数应该传什么。


Builder模式的对象创建,简单明了,更容易理解,而且流式的调用更加美观,不会出错。


从代码中可以看到,Computer类的构造函数是私有的,保证了所有对象的创建都必须从ComputerBuilder这个类来创建。且ComputerBuilder这个类的build方法中,可以进行校验或者其他操作。


同时,Computer这个类中是否存在Set方法,由你的实际应用场景决定,反正我的使用场景里,没有修改需求。


注意事项



  1. 上述代码为常见写法,并非固定模板。只要能通过Builder类创建目标对象,都可以算是建造者模式

  2. 建造者模式中的目标对象的构造函数必须是private修饰。否则可以直接创建对象。Builder类就没有意义了

  3. 建造者模式中的目标对象是否需要Set方法,由具体需求决定。一般情况下没有Set方法,可以避免对该对象中的参数进行修改。

  4. Builder中的build方法,可以处理一些逻辑问题,比如校验信息等

  5. 工厂模式注重的是同一类型的对象中通过参数来控制具体创建哪个对象。Builder模式关注的是单一对象中的参数传递

作者:OpenGL
链接:https://juejin.cn/post/7197326179934191676
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从自由职业到小公司再到港企再到国企,辛酸2022

今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。 自由职业总结 在此之前我先捋一下时间线,我是我从2021年6月份决定出来自由职业,起初跟朋友一起合作做点小生意,后来因为一些意见不合,从2022年2月份就退出了,2月份到5月份也在做很多尝试...
继续阅读 »

今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。


自由职业总结


在此之前我先捋一下时间线,我是我从2021年6月份决定出来自由职业,起初跟朋友一起合作做点小生意,后来因为一些意见不合,从2022年2月份就退出了,2月份到5月份也在做很多尝试,比如做剧本杀写作,自媒体卖书,接私单,但是经过考虑,做这些收入不稳定,而且回本周期比较长,有回款压力,而且之前创业的9个月里面,我也没赚到什么钱,倒是把自己的老本都吃光了,还透支了xy卡,还有一些wd,每个月都有还款压力。


所以在这里奉劝各位想做自由职业的朋友,如果不想打工,想出来自己创业,要三思啊,要起码满足以下几个条件:



1、有稳定发展的副业,而且副业的收入连续三个月以上超过主业收入

2、副业因为主业的影响而发展受限 3、自身有起码一年以上的周转资金,起码保证哪怕一年没收入也不至于饿死



而我很明显以上三点都不满足,到了后面实在没啥钱,业务也没有做起来,就动用了网贷,不过幸好及时止损,回归职场,现在细想,这是非常危险的,也是非常不理智的。


有条件千万不要dk创业,不要负z创业,到了后面心态真的绷不住,压力太大了,人很焦虑不说,事情还总办不好。



后来回来上班,第二个月领到第一笔工资,有时候摸鱼一天都有钱进账,多爽啊哈哈。


当然此次创业也给了一个教训就是尽量不要合伙创业,做成了还好,做不成就连朋友都真做不了,一地鸡毛,有机会好好讲一下这一年我自由职业的个人心得。


自由职业告一段落,现在进入职场时间。


回归职场


2022年6月到2022年12月这段时间也是比较动荡的。


不过也在意料之内,突然从自由职业回归到职场,还是会有阵痛期。


2022年6月-2022年9月,在一家小创业公司做前端负责人,薪资16k(我直接喊得,老板很爽快地答应了,怀疑是叫少了)。



这家公司技术栈是Vue2.x,业务有PC端应用,小程序应用,还有flutter开发桌面端。



但是因为技术生疏和对于业务方面不够娴熟,达不到老板的要求,9月8日被辞退了。


但是我没有气馁,心想要不再尝试一下自由职业吧,于是又花了14天时间去写剧本杀,想着靠剧本杀来翻盘,但是我的稿子被编剧无情打回来修改后,看着日进逼近的还款日期,还有自己手上不多的余粮,妈呀,立马又屁颠屁颠去准备面试,宝宝心里苦啊。



于是又火急火燎地边准备面试题边去面试,好在上天眷顾,10月22日入职了一家港企,也算是外企吧,薪资是16k,但是加班费奇高,就是我之前说的100元/小时。


因为公司是千人以上的大公司,所以业务线非常庞杂,技术栈也非常繁杂:



Vue3.0开发表单引擎 

React Native开发业务汇报APP 

Vue2.x+Electron开发桌面端应用 

Angular 

......



真可谓是前端大杂烩,不过眼下要还钱,虽然没有争取到涨薪,但起码有加班费,还好还好,再看一眼我的存款还有还款日期,没办法,就你了。


于是开始了疯狂卷模式,我在这家公司也是出了名的卷,以至于我现在离职快一个月了,公司还留存着我的光辉事迹......


为什么我又双叒离职了呢?



原因是我进去没多久,就由自愿加班转变成强制加班了,强制加班到9点半。


不过为了还钱,这点也可以接受吧。


不过最可怕的是,他们会给超出你工作能力的工作量,而且狂砍开发周期,比如我用react native从零开发一个系统,我原本估计要20天时间(保守一点),但是上层直接砍半,直接给10天!!


我艹,从入门到项目上线只给10天,没得办法,谁让我还在试用期,也不敢造次。


于是就开始跟另一个小伙伴开始摸天黑地的开发工作,连续10天都是凌晨1点才到家,第二天8点还得起床去上班。


然而10天根本不可能完整完成一个系统,我们连react native的基本知识都没搞懂,但是另外一个小伙伴说,尽力而为,实在不行就跑路。


听他这么说,我表面不说什么,内心那叫一个苦啊。


原来一个人有了负债就不再是你自己了,失去了那么多选择权,幸好这点负债对我来说压力不算太大,真想不懂那些有房贷车贷的人是怎么想的,那压力真的翻倍啊。


以后买房真的要慎之又慎!!



10天之后,我们两个人拼尽全力了,都还是没有办法按时上线,于是领导又给多了一周时间开发,并且放出狠话:



这一次要是再延期上线,就有人要被释放了!!



哎,没办法,再难受也要顶硬上。但是我转念一想,要是实在没办法完成,要拿我开刀,那怎么办??


不行,我不能做砧板上的鱼肉,我要选好退路,那就是继续去面试找下家,即使没办法上线他们要开掉我,我有offer在身,我也不需要担心那么多。


于是我从12月10日开始,屏蔽掉了现公司,开始了BOSS上海投之旅。


我当时是这么打算的,下一家公司要满足以下条件:



1、薪资必须要能够覆盖掉我的日常开支+还贷,还能存下一点钱抵抗后续风险 

2、至少稳定工作一年以上 

3、正常上下班,或者加班不多,多出来时间提升技术(技术真的跟不上了)



综上只有两种公司满足我的条件:



1、国企 

2、外企



有点讽刺,在大陆,最遵守劳动法的公司反而是外企。


但是面试我是不管那么多的,外面行情也不是那么好,但是幸运的是我比较注重简历包装,以及对于简历上可能问道的项目技术难点或者重点,甚至可能延伸出去的技术点,我都有做好非常严谨的准备,谁让我一路以来都在准备面试(其实是工作不稳定),所以还是很幸运在一周之内拿了不少offer,除了大厂(估计大厂嫌弃我简历太花了,没让我过,也可能是太菜了)


大厂,等我这波缓过来,一年以后或者两年以后我还是会冲的。


后来一周开发结束之后,急急忙忙上线,因为时间紧急,所以我们内部测试流程基本跑通就匆匆上线了,上线的当天测试测出60多个bug!!



企业微信被测试疯狂轰炸,我的另一个伙伴前几天跑路了,就只剩我一个人,有点难顶,于是领导又给我安排了另一个前端来帮忙,正好,等我把tapd上面的bug全部修复,二次测试通过之后,就甩锅给新来的前端,留下一纸技术交接文档还有离职申请,拍拍屁股去下家公司入职了,也算是对得起前公司了吧。



说实话,不是我扛不住压力,而是我真的不喜欢领导总是以释放,开除等等来给我们施压,我不是牛马,我也是人,是人就应该得到尊重!


万一我下次项目真的没办法上线,就把我开了,那我的处境就会非常被动了。


介绍一下我的新公司,大型的国企,流程正规,即使项目需求再赶也不至于把人给逼进绝路,正常上下班,大家都是到点走,有一次我稍微坐久一点,技术经理还过来提醒我没事可以早点走,劳逸结合,真正的人性化哈哈。


薪资也提高了一点,加班也是1:1,而且加班机会非常少,那多出来的时间,我可以有条不紊地提升技术。


一切都朝着好的方向发展,而且会越来越好。


说了那么多2022年,下面是我对于2023年的新年期望。


2023年新年期望


第一,当然是早日还清债务,现在的钱还不是我的,等还清贷款后,才是属于我的,起码现在我是这么认为的;


第二,从零开始重新钻研技术,这段时间也在根据自己的定位重新制定职业规划,后续会公布到这里;


经历过这次自由职业,我深刻地意识到,要想做成事,能力,经验,人脉,资本,缺一不可,而这些资源,都集中在大厂,只有去大厂,才可以完成自己的各项积累,即使进去之后,也不可以躺平,得过且过,要自己牢牢把握主动权。


所以2023年所做的一切都是为了进大厂做储备;


第三,当然是希望收获一段有结果的感情啦,虽然不知道是不是你,但是我还是会用心去经营,不辜负任何一个人,毕竟你有一点很吸引我,就是你身上闪烁着女性独立之光;


第四,就是把自己的技术沉淀到公众号,视频号,小红书,做自媒体输出,要是能够做成像月哥,神光,卡颂这种小网红就更好了哈哈,当然做这些注定前期是不赚钱的,降低期望值,逐步提升个人影响力,赚以后的钱吧。


而且我这个人天生脸皮厚,有旺盛的表达欲,又充满了乐观主义色彩,尽管去做吧,做技术输出,这绝对是稳赚不赔的买卖。


祝大家新年快快乐,万事如意,早日实现自己的人生目标!


作者:林家少爷
链接:https://juejin.cn/post/7190757076409253948
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS推送证书不受信任

问题:iOS推送证书不受信任 问题分析: 苹果已经使用了新的签名证书。 原文: Apple Worldwide Developer Relations Intermediate Certificate Expiration 解决方法: 打开苹果官方证书下载链接...
继续阅读 »

问题:iOS推送证书不受信任



问题分析:


苹果已经使用了新的签名证书。

原文: Apple Worldwide Developer Relations Intermediate Certificate Expiration


解决方法:


打开苹果官方证书下载链接:Apple PKI




下载G4证书,安装一下就可以了

收起阅读 »

Android深思如何防止快速点击

前言其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。1. AOP可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。...
继续阅读 »

前言

其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。

1. AOP

可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。

AOP来解决这类问题其实是近些年一个比较好的思路,包括比如像数据打点,通过AOP去处理,也能得到一个比较优雅的效果。牛逼的人甚至可以不用别人写的框架,自己去封装就行,我因为对这个技术栈不熟,这里就不献丑了。
总之,如果你想快速又简单的处理这种问题,AOP是一个很好的方案

2. kotlin

使用kotlin的朋友有福了,kotlin中有个概念是扩展函数,使用扩展函数去封装放快速点击的操作逻辑,也能很快的实现这个效果。它的好处就是突出两个字“方便”

那是不是我用java,不用kotlin就实现不了kotlin这个扩展函数的效果?当然不是了。这让我想到一件事,我也有去看这类问题的文章,看看有没有哪个大神有比较好的思路,然后我注意到有人就说用扩展函数就行,不用这么麻烦。

OK,那扩展函数是什么?它的原理是什么?不就是静态类去套一层吗?那用java当然能实现,为什么别人用java去封装这套逻辑就是麻烦呢?代码不都是一样,只不过kotlin帮你做了而已。所以我觉得kotlin的扩展函数效果是方便,但从整体的解决思路上看,缺少点优雅。

3. 流

简单来说也有很多人用了Rxjava或者kotlin的flow去实现,像这种实现也就是能方便而已,在底层上并没有什么实质性的突破,所以就不多说了,说白了就是和上面一样。

4. 通过拦截

因为上面已经说了kt的情况,所以接下来的相关代码都会用java来实现。
通过拦截来达到防止快速点击的效果,而拦截我想到有2种方式,第一种是拦截事件,就是基于事件分发机制去实现,第二种是拦截方法。
相对而言,其实我觉得拦截方法会更加安全,举个场景,假如你有个页面,然后页面正在到计算,到计算完之后会显示一个按钮,点击后弹出一个对话框。然后过了许久,改需求了,改成到计算完之后自动弹出对话框。但是你之前的点击按钮弹出对话框的操作还需要保留。那就会有可能因为某些操作导致到计算完的一瞬间先显示按钮,这时你以迅雷不及掩耳的速度点它,那就弹出两次对话框。

(1)拦截事件

其实就是给事件加个判断,判断两次点击的时间如果在某个范围就不触发,这可能是大部分人会用的方式。

正常情况下我们是无法去入侵事件分发机制的,只能使用它提供的方法去操作,比如我们没办法在外部影响dispatchTouchEvent这些方法。当然不正常的情况下也许可以,你可以尝试往hook的方向去思考能不能实现,我这边就不思考这种情况了。

public class FastClickHelper {

   private static long beforeTime = 0;
   private static Map<View, View.OnClickListener> map = new HashMap<>();

   public static void setOnClickListener(View view, View.OnClickListener onClickListener) {
       map.put(view, onClickListener);
       view.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               long clickTime = SystemClock.elapsedRealtime();
               if (beforeTime != 0 && clickTime - beforeTime < 1000) {
                   return;
              }
               beforeTime = clickTime;

               View.OnClickListener relListener = map.get(v);
               if (relListener != null) {
                   relListener.onClick(v);
              }
          }
      });
  }

}

简单来写就是这样,其实这个就和上面说的kt的扩展函数差不多。调用的时候就

FastClickHelper.setOnClickListener(view, this);

但是能看出这个只是针对单个view去配置,如果我们想其实页面所有view都要放快速点击,只不过某个view需要快速点击,比如抢东西类型的,那肯定不能防。所以给每个view单独去配置就很麻烦,没关系,我们可以优化一下

public class FastClickHelper {

   private Map<View, Integer> map;
   private HandlerThread mThread;

   public void init(ViewGroup viewGroup) {
       map = new ConcurrentHashMap<>();
       initThread();
       loopAddView(viewGroup);

       for (View v : map.keySet()) {
           v.setOnTouchListener(new View.OnTouchListener() {
               @Override
               public boolean onTouch(View v, MotionEvent event) {
                   if (event.getAction() == MotionEvent.ACTION_DOWN) {
                       int state = map.get(v);
                       if (state == 1) {
                           return true;
                      } else {
                           map.put(v, 1);
                           block(v);
                      }
                  }
                   return false;
              }
          });
      }
  }

   private void initThread() {
       mThread = new HandlerThread("LAZY_CLOCK");
       mThread.start();
  }

   private void block(View v) {
       // 切条线程处理
       Handler handler = new Handler(mThread.getLooper());
       handler.postDelayed(new Runnable() {
           @Override
           public void run() {
               if (map != null) {
                   map.put(v, 0);
              }
          }
      }, 1000);
  }

   private void exclude(View... views) {
       for (View view : views) {
           map.remove(view);
      }
  }

   private void loopAddView(ViewGroup viewGroup) {
       for (int i = 0; i < viewGroup.getChildCount(); i++) {
           if (viewGroup.getChildAt(i) instanceof ViewGroup) {
               ViewGroup vg = (ViewGroup) viewGroup.getChildAt(i);
               map.put(vg, 0);
               loopAddView(vg);
          } else {
               map.put(viewGroup.getChildAt(i), 0);
          }
      }
  }

   public void onDestroy() {
       try {
           map.clear();
           map = null;
           mThread.interrupt();
      } catch (Exception e) {
           e.printStackTrace();
      }
  }

}

我把viewgroup当成入参,然后给它的所有子view都设置,因为onclicklistener比较常用,所以改成了设置setOnTouchListener,当然外部如果给view设置了setOnTouchListener去覆盖我这的set,那就只能自己做特殊处理了。

在外部直接调用

FastClickHelper fastClickHelper = new FastClickHelper();
fastClickHelper.init((ViewGroup) getWindow().getDecorView());

如果要想让某个view不要限制快速点击的话,就调用exclude方法。这里要注意使用完之后释放资源,要调用onDestroy方法释放资源。

关于这个部分的思考,其实上面的大家都会,也基本是这样去限制,但是就是即便我用第二种代码,也要每个页面都调用一次,而且看起来,多少差点优雅。

首先我想的办法是在事件分发下发的过程去做处理,就是在viewgroup的dispatchTouchEvent或者onInterceptTouchEvent这类方法里面,但是我简单看了源码是没有提供方法出来的,也没有比较好去hook的地方,所以只能暂时放弃思考在这个下发流程去做手脚。

补充一下,如果你是自定义view,那肯定不会烦恼这个问题,但是你总不能所有的view都做成自定义的吧。

其次我想怎么能通过不写逻辑代码能实现这个效果,但总觉得这个方向不就是AOP吗,或者不是通过开发层面,在开发结束后想办法去注入字节码等操作,我觉得要往这个方向思考的话,最终的实现肯定不是代码层面去实现的。

(2)拦截方法

上面也说了,相对于拦截事件,假设如果都能实现的情况下,我更倾向于去拦截方法。

因为从这层面上来说,如果实现拦截方法,或者说能实现中断方法,那就不只是能做到防快速点击,而是能给方法去定制相对应的规则,比如某个方法在1秒的间隔内只能调用一次,这个就是防快速点击的效果嘛,比如某个方法我限制只能调一次,如果能实现,我就不用再额外写判断这个方法调用一次过后我设置一个布尔类型,然后下次调用再判断这个布尔类型来决定是否调用,

那现在是没办法实现拦截方法吗?当然有办法,只不过会十分的不优雅,比如一个方法是这样的。

public void fun(){
   // todo 第1步
   // todo 第2步
   // todo ......
   // todo 第n步
}

那我可以封装一个类,里面去封装一些策略,然后根据策略再去决定方法要不要执行这些步骤,那可能就会写成

public void fun(){
   new FunctionStrategy(FunctionStrategy.ONLY_ONE, new CallBack{
       @Override
       public void onAction() {
           // todo 第1步
           // todo 第2步
           // todo ......
           // todo 第n步
      }
  })
}

这样就实现了,比如只调用一次,具体的只调用一次的逻辑就写在FunctionStrategy里面,然后第2次,第n次就不会回调。当然我这是随便乱下来表达这个思路,现实肯定不能这样写。首先这样写就很不优雅,其次也会存在很多问题,扩展性也很差。

那在代码层面还有其它办法拦截或者中断方法吗,在代码层还真有办法中断方法,没错,那就是抛异常,但是话说回来,你也不可能在每个地方都try-catch吧,不切实际。

目前对拦截方法或者中断方法,我是没想到什么好的思路了,但是我觉得如果能实现,对防止快速点击来说,肯定会是一个很好的方案。

作者:流浪汉kylin
来源:https://juejin.cn/post/7197337416096055351

收起阅读 »

开始!使用node搭建一个小页面

web
介绍 这个小demo是Node.js, Express, MongoDB & More: The Complete Bootcamp系列课程的第一个demo,本篇文章主要介绍实现过程以及可能带来的思考。 完成展示 首页 详情页面 前置知识 首先我们...
继续阅读 »

介绍


这个小demo是Node.js, Express, MongoDB & More: The Complete Bootcamp系列课程的第一个demo,本篇文章主要介绍实现过程以及可能带来的思考。


完成展示


首页


image.png


详情页面


image.png


前置知识


首先我们需要了解一些知识,以便完成这个demo


fs


首先是node对文件的操作,也就是fs模块。本文只介绍一些简单的操作,大部分是例子中需要用到的方法。想要了解更多可以去API文档去查找。


首先引入fs模块:const fs = require("fs");


readFileSync


const textIn = fs.readFileSync("./txt/append.txt", "utf-8");
复制代码

上面代码展示的是readFileSync的使用,两个参数中,第一个参数是要读取文件的位置,第二个参数是编码格式encoding。如果指定encoding返回一个字符串,否则返回一个Buffer


writeFileSync


fs.writeFileSync("./txt/output.txt", textOut);
复制代码

writeFileSync毫无疑问是写文件,第一个参数为写文件的地址,第二个参数是写入的内容。


readFile、writeFile


上面的两个API都是同步的读写操作。但是nodeJs作为一个单线程的语言,在很多时候,使用同步的操作会造成不必要的拥堵。例如等待用户输入这类I/O操作,就会浪费很多时间。所以 js中有异步的方式解决这类问题,nodejs也一样。通过回调的方式来解决。


fs.readFile("./txt/append.txt", "utf-8", (err, data) => {
fs.writeFile("./txt/final.txt", `${data}`, (err) => {
console.log("ok");
});
});
复制代码

http


createServer


http.createServer(requestListener);
复制代码

http.createServer() 方法创建一个HTTP Server 对象,参数requestListener为每次服务器收到请求时要执行的函数。


server.listen(8001, "127.0.0.1", () => {
console.log("Listening to requests on port 8001");
});
复制代码

上面表代码表示监听8001端口。


url


url.parse


这个模块可以很好的处理URL信息。比如当我们请求http://127.0.0.1:8001/product?id=0的时候通过url.parse可以获取到很多信息。如下图:


image.png


实现过程


对于已经给出的完成页面,我们可以看到在切换页面时URL的变化,所以我们需要得到用户请求时的 URL地址,并根据地址展示不同的页面。所以我们通过path模块得到pathname,进行处理。


对于不同的请求,我们返回不同的界面。首先对于Overview page界面,由于它的类型是 html界面,所以我们通过writeHead将它的Content-type设置为text/html


res.writeHead(200, {
"Content-type": "text/html",
});
复制代码

其他的几个返回html的页面也是同样的处理。由于前端界面已经给出,我们只需要读取JSON里面的数据,并将模板字符串替换即可。最后我们通过res.end(output)返回替换后的页面。


总结


通过这一个小页面的练习,可以学习到node对文件的操作以及HTTP模块的操作。并对后端有了初步的认识。


作者:Yuki_
链接:https://juejin.cn/post/7171295946372972557
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一篇文章告诉你 <按钮> 如何选择,div button 还是a?

web
前言 当你要创建一个可点击的元素时,是使用 a标签 、button按钮 还是 div 等其他不同的元素? // 🚩 export function MyButton() { return <div onClick={...}>点击我</d...
继续阅读 »

前言


当你要创建一个可点击的元素时,是使用 a标签button按钮 还是 div 等其他不同的元素?


// 🚩
export function MyButton() {
return <div onClick={...}>点击我</div>
}
//❓
export function MyButton() {
return <button onClick={...}>点击我</button>
}
//❓
export function MyButton() {
return <a onClick={...}>点击我</a>
}
复制代码

出人意料的是大多数人都会选择div,这似乎与我们所学的很不一样。


这篇文章将展开对比三者的区别,并做了一个总结,这对于你工作或者面试的时候是很有帮助的。


div


让我们首先弄清楚一件事


您不应该将 div 用于可点击的元素(至少在 99% 的情况下)。


为什么?


严格上来说, div != 按钮。 div 只是一个通用容器,缺少一些可正确点击的元素应具备的特性,例如:



  • Div 不可聚焦,例如, tab 键不会像设备上的任何其他按钮那样聚焦 div。

  • 屏幕阅读器和其他辅助设备不会将 div 识别为可点击元素。

  • Div 不会将某些键输入(如空格键或返回键)转换为获得焦点时的点击。


但是,您可以使用 tabindex="0" 和 role=”button” 等几个属性解决其中一些问题:


// 🚩 试着将 div 改造成像 button一样...
export function MyButton() {
function onClick() { ... }
return (
<div
className="my-button"
tabindex="0" // 让div 能聚焦
role="button" // 屏幕阅读器和其他辅助设备 识别可点击
onClick={onClick}
onKeydown={(event) => {
// 聚焦时监听 回车键和空格键
if (event.key === "Enter" || event.key === "Space") {
onClick()
}
}}
>
点击我
</div>
)
}
复制代码

是的,我们需要确保设置聚焦状态的样式,以便用户反馈该元素也被聚焦。我们必须确保这通过了所有问题可访问性,例如:


.my-button:focus-visible {
outline: 1px solid blue;
}
复制代码

如果要还原所有细微且关键的按钮行为,并手动实现,需要大量工作。


button


The beauty of the button tag is it behaves just like any other button on your device, and is exactly what users and accessibility tools expect.

button 标签的美妙之处在于它的行为与您设备上的任何其他 button 一样,并且正是用户和辅助工具所期望的。


它是可聚焦的、可访问的、可键盘输入的,具有兼容的焦点状态样式!


// ✅
export function MyButton() {
return (
<button onClick={...}>
点击我
</button>
)
}
复制代码

有几个我们需要注意的问题。


button 的问题


我一直对按钮最大的烦恼是它们的样式


例如,给按钮一个浅紫色背景:


<button class="my-button">
Click me
</button>
<style>
/* 🤢 */
.my-button {
background-color: purple;
}
</style>
复制代码

这看起来就像 Windows 95 一样的样式。


A pretty ugly button


这就是为什么我们都喜欢 div。它们没有额外的样式或默认行为。它们的工作和外观每次都完全符合预期。


你可以说, appearance: none 会重置外观!但是这并不能完全按照您的想法进行。


<button class="my-button">
Click me
</button>
<style>
.my-button {
appearance: none; /* 🤔 */
background-color: purple;
}
</style>
复制代码

它仍然是这样:


A pretty ugly button


重置 button 的样式


没错,我们必须对每一个样式属性逐行重置:


/* ✅ */
button {
padding: 0;
border: none;
outline: none;
font: inherit;
color: inherit;
background: none
}
复制代码

这就是一个样式和行为都像 div 的按钮,它仍然使用浏览器的默认焦点样式。


您的另一种选择是使用 all: unset 恢复一个简单属性中的无特殊样式:


/* ✅ */
button { all: unset; }
button:focus-visible { outline: 1px solid var(--your-color); }
复制代码

但是不要忘记添加您自己的焦点状态;例如,您的品牌颜色的轮廓具有足够的对比度。


修复 button 行为属性


使用 button 标签时需要注意最后一个问题。


默认情况下, form 内的任何按钮都被视为提交按钮,单击时将提交表单


function MyForm() {
return (
<form onSubmit={...}>
...
<button type="submit">Submit</button>
{/* 🚩 点击 "Cancel"仍然会提交表单! */}
<button onClick={...}>Cancel</button>
</form>
)
}
复制代码

没错,按钮的默认 type 属性是 submit 。很奇怪。而且很烦人


解决此问题,除非您的按钮实际上是为了提交表单,否则请始终向其添加 type="button" ,如下所示:


export function MyButton() {
return (
<button
type="button" // ✅
onClick={...}>
Click me
</button>
)
}
复制代码

现在我们的按钮将不再尝试找到它们最接近的 form parent 并提交它。


哇,配置一个简单的按钮几乎变得奇怪了。


a标签 链接


这是大部分人也不注意的一点。我们使用按钮链接到其他页面:


// 🚩
function MyLink() {
return (
<button
type="button"
onClick={() => {
location.href = "/"
}}
>
Don't do this
</button>
)
}
复制代码

使用 点击事件 链接到页面的按钮的一些问题:



  • 它们不可抓取,因此对 SEO 非常不利。

  • 用户无法在新标签页或窗口中打开此链接;例如,右键单击在新选项卡中打开


因此,我们不要使用按钮进行导航。这就是我们需要 a 标签。


// ✅
function MyLink() {
return (
<a href="/">
Do this for links
</button>
)
}
复制代码

a 标签具有按钮的所有上述优点——可访问、可聚焦、可键盘输入——而且它们没有一堆默认的样式!


那我们是否应该将它们用于任何可点击的东西为我们自己省去一些麻烦?


// 🚩
function MyButton() {
return (
<a onClick={...}>
Do this for links
</a>
)
}
复制代码

不行


这是因为没有 href属性 的 a 标签不再像按钮一样工作。没错,当它 href 属性有值时,才有完整的按钮行为,例如可聚焦... 。


所以,我们一定要坚持使用按钮作为按钮,使用锚点作为链接。


buttona 结合起来


我非常喜欢的是将这些规则封装在一个组件中,这样你就可以只使用你的 MyButton 组件,


如果你 提供一个 URL,它就会变成一个链接,否则就是一个按钮就像这样:


// ✅
function MyButton(props) {
if (props.href) {
return <a className="my-button" {...props} />
}
return <button type="button" className="my-button" {...props} />
}

// 渲染出一个 <a href="/">
<MyButton href="/">Click me</MyButton>

// 渲染出 <button type="button">
<MyButton onClick={...}>Click me</MyButton>
复制代码

这样,无论按钮的用途是单击处理程序还是指向另一个页面的链接,我们都可以获得一致的开发人员体验和用户体验。


总结




  • 对于链接,使用带有 href 属性的 a标签,




  • 对于所有其他按钮,使用带有 type="button" 的 button 标签。




  • 需要一个点击容器,就用 div 标签


作者:前端代码王
链接:https://juejin.cn/post/7197995910566740025
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

疫情过后的这个春招,真的会回暖吗?

哈喽大家好啊,我是Hydra。今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪个景...
继续阅读 »

哈喽大家好啊,我是Hydra。

今天是正月初七,不知道大家有没有复工,反正我今天已经坐在办公室里开始码字了。

这个春节假期相信大家过的都不错,可以看到今年无论是回家探亲、还是外出旅游的人数,都比疫情放开前两年有了爆发式的增长。假期我躺在被窝里刷抖音,每当刷到哪个景点人满为患到走不动路的时候,都觉得自己宅在家里哪也不去真的是太对了。

好了回归正题,很多小伙伴们非常关注的一个问题,在经历了疫情放开、大规模感染的相对平稳后,这一届春招真的会回暖吗?

在聊春招之前,我觉得还是有必要再来科普一下春招的时间线。

  • 12月,一般只有少量的企业开始进行春招提前批预热,或是进行秋招的补录
  • 1月,部分公司开启春招正式批
  • 3-4月,才是春招的高峰期,大部分公司在这个时间段陆续开启春招
  • 5月,大部分的企业会结束招聘

为了了解今年的形势,我也逛了不少论坛,了解到有一些大厂在去年12月底的时候,就已经开始了秋招的补录,不少人收到了补录的通知。

通过整体氛围来看,今年春招大概率会比去年进行一波升温,在岗位的可选择性上,大伙可能也有更多的大厂岗位可以进行一波冲击。尽管如此我还是劝大家要尽早准备,因为虽然说是春招,但并不是真正到了春天才真正开始,并且春招的难度比秋招可能还要高上不少。

首先,相对于秋招来说,春招的岗位会少很多,因为春招更多是对于秋招的补充,是一个查漏补缺的过程,对秋招中没有招满、或者有新岗位出现的情况下,才会在春招中放出该岗位。少量的岗位,需要你能更好的把握信息资源,迅速出击。

其次,你可能拥有更多的竞争对手,考研、考公失利的同学如果不选择二战,将会大量涌入春招,而对于秋招找到的工作不满意想要跳槽的同学,有笔试面试经验、工作经历,将会成为你春招路上麻烦的对手。

所以说到底,大家还是不要过于盲目乐观,扎扎实实的准备肯定是不能少的,毕竟春招的难度摆在这里。在看到大规模补录的同时,我们也不能否认背后的裁员依旧存在。有可能你现在看到的hc,就是在不久前刚刚通过裁员所释放的。

另外,我还是得说点泼冷水的话,虽然看上去形势一片大好,岗位放开了很多,但不代表薪资待遇还是和以前一样的,从一个帖子中可以看到,即便是在杭州和成都的中厂里,降薪也是存在的。

因为说到底,疫情并不是经济下行的根本原因,想要寄希望于疫情放开后经济能够快速复苏基本是不可能的。

国内的互联网公司,已经过了那个爆发式发展的黄金时期,甚至说一句互联网公司规模已经能隐隐约约窥到顶峰也不过分。美联储加息、中概股暴跌、企业融资困难…面对这些困难的环境,即使疫情放开也于事无补。

尽管环境如此困难,我仍然认为互联网行业是小镇做题家们快速实现社会价值、积累财富的黄金职业。看看大厂里十几万、几十万的年终奖,并不是每个行业都能做到的。

最后还是建议大家,积极准备,不管这个春招是否回暖,还是要做到尽量不留遗憾,不要给自己找借口,再寄希望于下一个秋招。

2023年,我们一起加油!


作者:码农参上
链接:https://juejin.cn/post/7193885908129546277
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

卷王都在偷偷准备金三银四了

春节长假的尾巴怕是抓不住了,那就安心等五一吧。 可一回到岗位上,熟悉的办公室味道,熟悉的同事,月薪8000,互相笑嘻嘻。 突然迎来老板的动员大会,今年蒸蒸日上,大家共创辉煌,抿一口蛋糕,大家都是一家人。 但仔细想了想,你还欠我年终奖呢。 回到几平米的屋子,用身...
继续阅读 »

春节长假的尾巴怕是抓不住了,那就安心等五一吧。


可一回到岗位上,熟悉的办公室味道,熟悉的同事,月薪8000,互相笑嘻嘻。


突然迎来老板的动员大会,今年蒸蒸日上,大家共创辉煌,抿一口蛋糕,大家都是一家人。


但仔细想了想,你还欠我年终奖呢。


回到几平米的屋子,用身子量了量床,打开某脉,某乎,果然大家都一样,年终奖没发;简历石沉大海;发消息只读不回。


顺手打开某招聘,看了看岗位,这个厂还不错,可是要求好高,我啥都不会。


“哎,算了,我简历还没更新呢,我躺到6月份拿到年终奖再跑路。”


“Timi~”


这是不是你的状态呢?我们习惯被一些悲观的环境因素所影响,以至于在网络上寻求共鸣,麻痹自己停滞不前,甚至看到别人比自己惨内心略显喜悦。


最终一年一年下来,玩没玩到,钱没赚到,反倒把自己给内耗住了。


故此,写一篇求职经验相关的文章,此文不对职业规划做文章,也不是鼓励跳槽,更不是和任何人作对。


而是,刚毕业求职,打算跳槽,已经离职进行求职的一个经验分享。


也是以我经验以及身边大佬的一些经验进行分享。这里包含面试前的心态调整、简历制作、面试沟通指导等内容。


文末更有简历模板,全面的面试资料,以及面试经验指导。


心态篇


第一个需要转变的就是心态问题。在这里我其实主张“平等相待,泰然处之”。


很多伙伴对于求职的心态总是低于其一端,但是其本质是你提供劳动力,对方提供相应的报酬,一拍即合的事,没有因为对方提供报酬你就对他畏首畏尾。


从我毕业到现在,没有任何一场因为自己菜,对面试官的提问答不出来,题目做不出来,但是对面试官毕恭毕敬,甚至低头哈腰而促成一个岗位。


很简单,面试官也是一个员工,负责技术把关。当你实力不行但是态度很OK的时候,面试官点头同意,进入岗位后所造成的一切损失,面试官也会面临连带作用,被质疑技术行不行,看人准不准。他们也渴望一场势均力敌的较量呢。


所以,我们尽可能的对阵面试官,都能把心态置于同一水平上,不卑不亢,切磋技术,聊经验,聊项目,全当朋友一样聊天。


技术栈势均力敌就多沟通探讨一点,他强就虚心请教并告知某某知识点还没过多接触,你强就大致内容讲述出来,点到为止不要秀技即可。


我相信面试官基本在公司具备一定权重的,或许有可能就是入职引导你的人。也是很讲礼貌的,基础的问候相信大家都明白,也不讲述,讲多了画风就变了。


摆平心态不是几句劝诫的话就能做到的。


我以前比较弱势,对人心态总是低于一端,语气小,讲的内容不够自信甚至紧张,一紧张很多马脚露出来了,一些知识点的也连不上,支支吾吾作答。


所以我在2022年就专门花了心思去解决这一问题,具体行动往期文章也有透露,但帮助最大的还是反复阅读 《庄子.内篇》 以及B站的视频注解,让我的内心平淡了很多,也看开了很多,内篇中比较出名的在我们高中课本《逍遥游》也有学到。



“至人无己,神人无功,圣人无名。”


所有的一切,领导也好,老板也好,大佬也好。都是自己的意识有意给人贴上的标签,本质都是人,都是生物,多细胞生物。


或许吃穿奢靡,身居庙堂,那又如何呢?不都是碳基生物吗?


描述有些不当,本意是让大家心态放开点,没有什么大不了的。


依我个人经验,在自己的心结开放了之后,在面试求职时,基本对等谈话,会的知识点就多说一点,对知识点不那么熟就借助经验和案例来分析,不会就讲明没有过多了解,不丢人。


在平等状态下聊天,很多知识点能够由点连成片,顺势探讨下去,不仅你的感觉良好,面试官体验也很棒。


我在2022年国庆结束后,裸辞后选择继续求职,深圳和杭州都有offer,更有甚者免试用期,14薪,增加薪资邀请加入他们。


在那一刻明白,企业在面对一个真正人才时不会吝啬待遇,关键你是不是他要找的人。


简历篇


简历,或许是每一位求职者的第一道门槛,一个简历能够看出你对这份职业的用心程度,和你的细心严谨程度。


为什么这么说呢?


前阵子也帮一些伙伴检查过和更改过简历,一路下来更新简历版本中,出现过错别字,排版不雅观,描述有误,甚至有时候邮箱,电话号码都写错过。


纠结简历石沉大海,只读不回的原因,往往是一些细节导致。有些 HR 对于文字是很严苛的,一见到细微地方不对,就会联想到候选人不够严谨。毕竟作为一名程序员,对数据严谨和信息敏感难道不是应该的吗?


基础信息,岗位经历描述切勿忽视,文字表达也是需要斟酌,完事后多审查几遍,这个只有靠自己的习惯和用心程度。


连自己的简历都看不到 5 遍,这是对自己的自信还是对自己经历不忍直视,何况给人改简历挑毛病都要阅读几遍呢。


那我们回归到简历排版上,选择排版上也推崇精简排版,把一些基础信息,工作经历,技术栈描述清楚就OK,并不需要多花里胡哨。


在描述专业技能时,根据自身情况描述清楚,注意一些技术名词的写法,有些技术严谨的人对于写法还是尤其在意的。


曾有一次自己写的一篇文章对一个技术英文写法不正确,一部分的人对我进行批评和纠正。所以有一些细微的细节在自己看来微乎其微甚至无所谓,但总是有人会持有不同看法。



▲图/ 简历基础信息示例一


又如下图,精简模板即可,把自己的基本信息描述清楚即可,谨慎出现错别字和联系方式信息错误等。


image.png


▲图/ 简历基础信息示例二


有伙伴咨询过我,如果自己是专升本的情况该怎么填写简历。


如果是以上情况,可以准备多份简历,一份简历头部的基础,学历为本科,院校为本科院校,在后面的教育背景一栏,则一行为本科院校,第二行为专科院校。


另一份,则简历头部基础信息填写本科的学校信息,教育背景这一栏清除不填写或者只写本科一栏。



▲图/ 简历教育信息示例一


如果是你心仪的岗位,根据岗位要求,发送相应要求的简历,先获取展示自己的机会再说。


至于项目经历,这可能是第二重点,一些岗位会根据你的经历来招聘,上手会快一些,比如一家企业的岗位物联网方向较多,那他更加倾向于熟悉在物联网设备上有相关经历的伙伴。


同时在面试时,面试官更多可能通过你的项目经历以及用到的技术栈来考察你。


这里有个小技巧就是,你的技术栈和项目经历可以按实际需求写,当你发现有一些岗位是你心仪但是你又没有相关经验之后,你的技术栈和项目经历里可以稍微加上匹配岗位的技术栈和技术使用经历,这里虽然给自己留了坑,但是在获取面试机会之后需要自己补充相关的知识点。



▲图/ 简历项目经历示例一



▲图/ 简历项目经历示例二


以上两个案例,描述一个项目经历的基本信息,包含项目是什么,怎么做,做了什么,你负责并担任了什么,你的收获又是什么。


通俗一点,就是不要以自己的角度去写,要给到面试官角度去写,让面试官通过你的项目经历了解你的能力和经历,知道这个项目的权重比重是否大,你又负责了多少职责以及使用了什么手段去完成这个项目。


自我介绍一栏,阐明自己的一些辅助优点,例如你的自我评价是怎样的一个人,对于团队、岗位你能够具备什么样的素质,以及业余会干嘛,是否有更迭技术等等。



▲图/ 简历自我评价示例一



▲图/ 简历自我评价示例二


另外,简历文件的格式一定要规范化,文件命名名为:姓名+岗位+学历+联系方式。 例如:桑小榆-开发岗-本科-1517258505。


这里有伙伴不规范原因就是以自身角度想法,打开一看就知道是你,但是没经历过永远不知道一个 HR 面对一群人挑选简历时的心酸,命名的规范化突出略显重要。


面试篇


前期的准备,都是在做铺垫,为的就是和面试官阵面对决,这绝对是一个综合素质的体现,展示技术情况,沟通实力以及心理素质情况。


对于前期心态的准备以及简历的准备,很幸运的被邀请到了面试,这时候你需要准备的就是对被邀请的企业背景了解和岗位的大致了解。


以至于不那么被动的和面试官尬聊:你有什么想要问的吗?


这里面试的环境,基本包含了笔试,机试,面聊等等。


对于笔试和机试,那一定是对于自身知识储备的考验了,这里需要我们自身去积累和回顾了。


面试造飞机,工作拧螺丝或许是很常见的行为,这也不得不让我们需要对笔试题和机试题的一些提前准备了。


找工作不容易,大家何尝不想当一名八股文选手,怕就怕有人连八股文的苦都不想吃。


这里我也准备了 Web 岗位,.NET 岗位,和 JAVA 岗位的面试资料,更有简历模板奉上,大家也可在文末查看。


对于面试时自我介绍,如果你能够很好的讲诉自己那不用说,如果不是很清晰,可以自己写好一段自我介绍,记熟悉就好,面试的时候围绕着写的内容可以很好的完成自我介绍。


对于技术面试,大多会围绕你简历所写的知识进行提问,尽可能的熟悉你的简历和所写到的技术栈,在回答问题时尽量引导你熟悉的技术范围内,不要炫技或者提到你听过但是不熟悉的知识点,这样引导下去将会很糟糕。


我曾经有一次就是回答 AOP 编程思想时,讲完一些大概内容之后还提到了框架的使用,结果对面直接提问如果不用框架,自己手动代码实现会怎么做,这就往困难的方向了,好在心态比较稳加上有过经验一点一点回答上了。


结束篇


如果,老东家不是那么抠门,我找啥自行车啊。


如果,老东家体恤员工的不易,我也不用每天花力气惦记上个月的工资还没发,年终奖还欠我呢。


如果,老东家足够爽快,我也不用每天猜测啥时候涨薪,也不用每天刷刷岗位,偷偷打电话。


哎呀,人与人之间咋就这么复杂呢~


如果,你的老东家亏待了你,或者你看不到未来了,你可以试试以下步骤:



金三银四路线


1.着手准备自己技术栈的复盘和技术栈更迭;


2.打开LeetCode,每日练习算法题,开拓思路;


3.查看相关岗位并更新自己的简历;


4.提前准备好自我介绍,几个提问的问题;


5.交接好手头工作,善待和你一样的打工人并告辞后赴任。


作者:桑小榆呀
链接:https://juejin.cn/post/7194980499222167609
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在国企做程序员怎么样?

有读者咨询我,在国企做开发怎么样? 当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 下面分享一位国企程序员的经历,希望能给大家一些参考价值。...
继续阅读 »

有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


作者:程序员大彬
链接:https://juejin.cn/post/7182355327076007996
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么onMeasure会被执行两次

什么情况下会onMeasure会执行? 进入View的measure方法: void measure(){ boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_...
继续阅读 »

什么情况下会onMeasure会执行?


进入Viewmeasure方法:


void measure(){
boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
boolean isSepcExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if(forceLayout || needLayout){
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
}
}

什么时候forceLayout=true:



  1. 调用requestLayout

  2. 调用forceRequestLayout


什么时候needsLayout=true:



  1. 当长宽发生改变


什么时候调用了onMeasure>方法:



  1. forceLayouy=true

  2. 或者mMeasureCache没有当前的缓存


所以总结:当调用了requestLayout一定会测发重测过程.当forceLayout=false的时候会去判断mMeasureCache值.现在研究下这个mMeasureCache


class View{
LongSparseLongArray mMeasureCache;
void measure(widthSpec,heightSpec){
---
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if(cacheIndex<0){
onMeasure(widthSpec,heightSpec);
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key,widhSpec|heightSpec);
---
}
}

这里可以看到oldWidthMeasureSpecmMeasureCache都是缓存上一次的值,那他们有什么不同呢?不同点就是,oldWidthMeasureSpec>不仅仅缓存了测量的spec模式而且缓存了size.但是mMeasureCache只缓存了size.从这行代码可以看出:


long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;

这里一同运算就为了排除掉spec造成的影响.


//不信你可以试下下面的代码
public class Test {
public static void main(String[] args) {
long widthMeasureSpec = makeMeasureSpec(10,0);
long heightMeasureSpec = makeMeasureSpec(20,0);
long ss = widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
System.out.println("=========="+ss);
}

private static final int MODE_MASK = 0x3 << 30;

public static int makeMeasureSpec(int size,
int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//42949672980
//42949672980
//42949672980



什么时候mPrivateFlags会被赋值PFLAG_FORCE_LAYOUT.


view viewGrouup的构造函数里面会主动赋值一次,然后在ViewGroup.addView时候会给当前ViewmProvateFlags赋值PFLAG_FORCE_LAYOUT.




为什么onMeasure会被执行两次?


void measure(int widthMeasureSpec,int heightMeasureSpec){
----
boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
if(forceLayout | needsLayout){
onMeasure()
}
----
}
public void layout(int l, int t, int r, int b){
---
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
---
}

在第一次触发到measure方法时,forceLayoyt=true needsLayout=true,但是layout方法还没触发到.

在第二次触发到measure>方法时,forceLayout=true needsLayout=false,所以还是会进入onMeasure方法.这次会执行layout方法.然后我们在下次的时候forceLayout就等于false了.上面的这一段分析是分析的measure内部如何防止多次调用onMeasure.


现在分析外部是如何多次调用measure方法的:

Activity执行到onResume生命周期的时候,会执行WindowManager.addView操作,WindowManager的具体实现类是WindowManagerImpl然后addView操作交给了代理类WindowManagerGlobal,然后在WindowManagerGlobaladdView里面执行了ViewRootImpl.setView操作(ViewRootImpl对象也是在这个时候创建的),在ViewRootImpl会主动调用一次requestLayout,也就开启了第一次的视图 测量 布局 绘制.


setView的时候主动调用了一次ViewRootImpl.requestLayout,注意这个requestLayoutViewRootImpl的内部方法,和view viewGroup那些requestLayout不一样.在ViewRootImpl.requestLayout内部调用了performTraversals方法:


class ViewRootImpl{
void performTraversals(){
if(layoutResuested){
//标记1
windowSizeMayChanged |= measureHierarchy(host,lp,res,desiredWindowWidth,desiredWindowHeight);
}
//标记2
performMeasure()
performLayout()
}
void measureHierarchy(){
performMeasure()
}
}

ViewRootImpl的执行逻辑你可以看出,在执行performLayout之前,他自己就已经调用了两次performMeasure方法.所以你现在就知道为啥了.


作者:Librity
链接:https://juejin.cn/post/7197116653195624508
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

多渠道打包配置和打包脚本修改

之前的文章 《创造 | 一个强大的 Android 自动化打包脚本》 介绍了 Android 自动打包脚本的配置。其实之前的脚本里还有些多渠道打包的配置实现方式并不好,比如使用 32 位还是 64 位 NDK 的问题。最近我对这个配置的打包做了更新。此外,因为...
继续阅读 »

之前的文章 《创造 | 一个强大的 Android 自动化打包脚本》 介绍了 Android 自动打包脚本的配置。其实之前的脚本里还有些多渠道打包的配置实现方式并不好,比如使用 32 位还是 64 位 NDK 的问题。最近我对这个配置的打包做了更新。此外,因为 Google Play 检测出我在应用里面使用了友盟的 SDK,违反了谷歌的开发者政策,所以我决定将海外版本的应用的崩溃信息统计切换到谷歌的 Firebase,因此也需要做多渠道的配置。


QQ截图20221119112054.png


1、针对不同 NDK 的区分


首先,针对使用不同 NDK 的配置,我将 Gradle 配置文件修改位通过外部传入参数的形式进行设置,具体的脚本如下,


android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
if (project.hasProperty("build_ndk_type") && build_ndk_type == "ndk_32") {
println(">>>>>>>> NDK option: using 32 bit version")
ndk {abiFilters 'armeabi-v7a', 'x86'}
} else if (project.hasProperty("build_ndk_type") && build_ndk_type == "ndk_32_64") {
println(">>>>>>>> NDK option: using 32 and 64 bit version")
ndk {abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64'}
} else {
// default is 64 bit version
print(">>>>>>>> NDK option: using 64 bit version")
ndk {abiFilters 'arm64-v8a', 'x86_64'}
}
}
}

这样,就可以通过打包命令的参数动态指定使用哪种形式的 NDK,


./gradlew resguardNationalDebug -Pbuild_ndk_type=ndk_32

2、针对海内和海外不同依赖的调整


这方面做了两个地方的调整。一个是因为对 Debug 和 Release 版本或者不同的 Flavor,Gradle 会生成不同的依赖命令,于是针对不同的渠道可以使用如下的命令进行依赖,


// apm
nationalImplementation "com.umeng.umsdk:common:$umengCommonVersion"
nationalImplementation "com.umeng.umsdk:asms:$umengAsmsVersion"
nationalImplementation "com.umeng.umsdk:apm:$umengApmVersion"
internationalImplementation 'com.google.firebase:firebase-analytics'
internationalImplementation platform("com.google.firebase:firebase-bom:$firebaseBomVersion")
internationalImplementation 'com.google.firebase:firebase-crashlytics'
// debugtools
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
debugImplementation 'com.github.Shouheng88:uetool-core:1.0'
debugImplementation 'com.github.Shouheng88:uetool-base:1.0'
releaseImplementation 'com.github.Shouheng88:uetool-no-op:1.0'
debugImplementation "com.facebook.stetho:stetho:$stethoVersion"
debugImplementation "com.iqiyi.xcrash:xcrash-android-lib:$xCrashVersion"

这里针对了 national 和 international 两个 flavor 分别使用了 nationalImplementation 和 internationalImplementation 两个依赖命令。此外,针对一些只在 Debug 环境中使用的依赖,这里使用了 debugImplementation 声明为只在 Debug 包里使用。


另一个调整是,因为比如如果我们只在 Debug 环境中或者个别渠道中使用某些依赖的话,那么当打 Release 包或者其他渠道的时候就可能出现依赖找不到的情况。这种情况的一种处理措施是像 leakcanary 一样,声明一个 no-op 的依赖,只包含必要的类文件而不包含具体的实现。此外,也可以通过下面的方式解决。


首先在项目中添加一个 module 或者使用已有的 module,然后已 CompileOnly 的形式引用上述依赖,


// apm
compileOnly "com.umeng.umsdk:common:$umengCommonVersion"
compileOnly "com.umeng.umsdk:asms:$umengAsmsVersion"
compileOnly "com.umeng.umsdk:apm:$umengApmVersion"
compileOnly 'com.google.firebase:firebase-analytics'
compileOnly platform("com.google.firebase:firebase-bom:$firebaseBomVersion")
compileOnly 'com.google.firebase:firebase-crashlytics'
// debugtool
compileOnly "com.facebook.stetho:stetho:$stethoVersion"
compileOnly "com.iqiyi.xcrash:xcrash-android-lib:$xCrashVersion"

这样是否使用某个依赖就取决于主 module. 然后,对需要引用到的类做一层包装,主 module 不直接调用依赖中的类,而是调用我们包装过的类。


object UmengConfig {

/** Config umeng library. */
fun config(application: Application, isDev: Boolean) {
if (!AppEnvironment.DEPENDENCY_UMENG_ANALYTICS) {
return
}
if (!isDev) {
UMConfigure.setLogEnabled(isDev)
UMConfigure.init(application, UMConfigure.DEVICE_TYPE_PHONE, "")
MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO)
}
}
}

这样,主 module 只需要在 application 里面调用 UmengConfig 的 config 方法即可。这里我们可以通过是否为 Debug 包来决定是否调用 Umeng 的一些方法,所以,这种方式可以保证打包没问题,只要 Release 版本调用不到 Umeng SDK 的类也不会出现类找不到的异常。此外,也可以通过如下方式


public class AppEnvironment {

public static final boolean DEPENDENCY_UMENG_ANALYTICS;
public static final boolean DEPENDENCY_STETHO;
public static final boolean DEPENDENCY_X_CRASH;

static {
DEPENDENCY_UMENG_ANALYTICS = findClassByClassName("com.umeng.analytics.MobclickAgent");
DEPENDENCY_STETHO = findClassByClassName("com.facebook.stetho.Stetho");
DEPENDENCY_X_CRASH = findClassByClassName("xcrash.XCrash");
}

private static boolean findClassByClassName(String className) {
boolean hasDependency;
try {
Class.forName(className);
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
return hasDependency;
}
}

即通过能否找到某个类来判断当前环境中是否引用了指定的依赖,如果没有指定的依赖,直接跳过某些类的调用即可。


用上面的方式即可以解决 Android 中的各种多渠道打包问题。


3、通过外部参数指定打包版本


这个比较简单,和配置 NDK 的形式类似,只需要通过判断指定的属性是否存在即可,


if (project.hasProperty("version_code")) {
println(">>>>>>>> Using version code: " + version_code)
versionCode = version_code.toInteger()
} else {
versionCode = rootProject.ext.versionCode
}
if (project.hasProperty("version_name")) {
println(">>>>>>>> Using version name: " + version_name)
versionName = version_name
} else {
versionName = rootProject.ext.versionName
}

这样配置之后打包的传参指令为,


./gradlew assembleNationalDebug -Pbuild_ndk_type=ndk_32 -Pversion_code=121 -Pversion_name=hah

这样打包的时候就无需修改 gradle 脚本,直接通过传参的形式打包即可,做到了真正的自动化。


4、打包脚本 autopackage 的一些更新


经过上述配置之后,我对 autopackage 打包脚本也相应地做了一些调整。


1、调用脚本的时候也支持外部传入参数,比如


python run.py -s config/config_product.yml -v 324 -n 3.8.1.2

用来指定打包的配置文件、版本号以及版本名称。其次对打包脚本的 NDK 和 Flavor 配置做了调整,本次使用枚举来声明,含义更加准确,


def assemble(bit: BitConfiguration, flavor: FlavorConfiguration) -> ApkInfo:
'''Assemble APK with bit and flavor and copy APK and mapping files to destination.'''
# ./gradlew assembleNationalDebug -Pbuild_ndk_type=ndk_32 -Pversion_code=322 -Pversion_name=3.8.0
assemble_command = "cd %s && gradlew clean %s -Pbuild_ndk_type=%s" \
% (config.gradlew_location, flavor.get_gradlew_command(), bit.get_gradlew_bit_param_value())
if len(build_config.version_code) != 0:
assemble_command = assemble_command + " -Pversion_code=" + build_config.version_code
if len(build_config.version_name) != 0:
assemble_command = assemble_command + " -Pversion_name=" + build_config.version_name
logi("Final gradlew command is [%s]" % assemble_command)
os.system(assemble_command)
info = _find_apk_under_given_directory(bit, flavor)
_copy_apk_to_directory(info)
_copy_mapping_file_to_directory(info, flavor)
return info

2、对 YAML 文件解析做了简化,调用方式将更加便捷,


class GlobalConfig:
def parse(self):
self._configurations = read_yaml(build_config.target_script)
self.publish_telegram_token = self._read_key('publish.telegram.token')

def _read_key(self, key: str):
'''Read key from configurations.'''
parts = key.split('.')
value = self._configurations
for part in parts:
value = value[part.strip()]
return value

3、生成 Git log 使用了标准的 Git 指令


首先,获取当前最新的 Git tag 使用了 Git 自带的指令,


git describe --abbrev=0 --tags

该指令可以以简单的形式输出最新的 tag 的名称。


此外,拿到了上述 tag 之后我们就可以自动获取提交到上一次提交之间的所有提交记录的信息。获取提交记录的指令也使用了 Git 自带的指令,


git log %s..HEAD --oneline

上述方式可以以更见简洁的代码实现自动生成当前版本 Git 变更日志的功能。


4、对 diff 输出的结果的展示进行了美化


之前发送邮件的时候使用的是纯文本,因为邮件系统的文字并不是等宽的,所以,导致了展示的时候格式化非常难看。本次使用了等宽的字体并且以 html 的形式发送邮件,相对来说输出的结果可视化程度更好了一些,


QQ截图20221119121158.png


以上就是脚本的更新,仓库地址是 github.com/Shouheng88/… 有兴趣自己参考。


作者:开发者如是说
链接:https://juejin.cn/post/7167576554816405512
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

高端操作!实现RecyclerView的上下拖拽

写在前面 最近工作强度好大,一天能敲10小时以上的代码,敲的我头疼。代码写多了,突然想起来,好像真的很久没发技术文了,原因有很多,就不说了。。都是借口,今天分享内容也是工作时遇上的一个小需求,觉得挺有意思,那就写篇文章吧!   需求描述大概是这样,一个页面有一...
继续阅读 »

写在前面


最近工作强度好大,一天能敲10小时以上的代码,敲的我头疼。代码写多了,突然想起来,好像真的很久没发技术文了,原因有很多,就不说了。。都是借口,今天分享内容也是工作时遇上的一个小需求,觉得挺有意思,那就写篇文章吧!

需求描述大概是这样,一个页面有一个列表,列表里有很多item,需要支持用户拖拽其中item到不同的位置,来对列表项进行重新排序。


要实现的效果大概如下:


1_实现效果演示


除去与业务相关的部分,我们只需关注如何让列表item支持上下拖拽就行,这也是这个需求的关键。



我们组安卓岗在半年前已经全部用kotlin进行开发了,所以后续我的文章也会以kotlin为主进行demo的编写。一些还没学过kotlin的朋友也不用担心,kotlin和java很像,只要你熟悉java,相信你也是可以看得懂的。



那么应该如何实现呢?我们需要写个接口去监听每个item的当前状态(是否被拖动)以及其当前所在的位置吗?不需要


得益于RecyclerView优秀的封装,系统内部默认提供了这样的接口给我们去调用。


ItemTouchHelper


简单介绍下这个类,系统将这些接口封装到了这个类里,看看这个类的描述,它继承自RecyclerView.ItemDecoration,实现了RecyclerView.OnChildAttachStateChangeListener接口。


public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {}

ItemDecoration这个类比较熟悉,它可以用来让不同的子View的四周拥有不同宽度/高度的offset,换句话说,可以控制子View显示的位置。


而OnChildAttachStateChangeListener这个接口,则是用来回调当子View Attach或Detach到RecyclerView时的事件。


那怎么使用这个ItemTouchHelper呢?


val callback = object : Callback {...}
val itemTouchHelper = ItemTouchHelperImpl(callback)
itemTouchHelper.attachToRecyclerView(mRecyclerView)

首先定义一个callback,然后传给ItemTouchHelper生成实例,最后将实例与recyclerView进行绑定。


ItemTouchHelper只负责与recyclerView的绑定,剩下的操作都代理给了callback处理。


callback内部实现了许多方法,我们只需要关注里面几个比较重要的方法


getMovementFlags()

callback内部帮我们管理了item的两种状态,一个是用户长按后的拖拽状态,另一个是用户手指左右滑动的滑动状态(以竖向列表为例),这个方法返回允许用户拖拽或滑动时的方向。


override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(dragFlags, swipeFlags)

比如我们希望,竖向列表时,禁止用户的滑动操作,仅支持用户上、下方向的拖拽操作


因此我们可以这样定义:


val dragFlags = (ItemTouchHelper.UP or ItemTouchHelper.DOWN)
val swipeFlags = 0 // 0 表禁止用户各个方向的操作,即禁止用户滑动

然后传入makeMovementFlags(),这个方法是callback默认提供的,我们不需要关注它的内部实现。


onMove()

当用户正在拖动子View时调用,可以在这里进行子View位置的替换操作


onSwiped()

当用户正在滑动子View时调用,可以在这里进行子View的删除操作。


isItemViewSwipeEnabled(): Boolean

返回值表是否支持滑动


isLongPressDragEnabled(): Boolean

返回值表是否支持拖动


onSelectedChanged(ViewHolder viewHolder, int actionState)

当被拖动或者被滑动的ViewHolder改变时调用,actionState会返回当前viewHolder的状态,有三个值:




  • ACTION_STATE_SWIPE:当View刚被滑动时返回




  • ACTION_STATE_DRAG:当View刚被拖动时返回




  • ACTION_STATE_IDLE:当View即没被拖动也没被滑动时或者拖动、滑动状态还没被触发时,返回这个状态




在这个方法我们可以对View进行一些UI的更新操作,例如当用户拖动时,让View高亮显示等。


clearView()

当View被拖动或滑动完后并且已经结束了运动动画时调用,我们可以在这里进行UI的复原,例如当View固定位置后,让View的背景取消高亮。


真正的开始


简单介绍完这个Callback,接下来写我们的代码


首先准备好我们的列表,列表不需要复杂,够演示就行,就放一行文字,代码我就不贴了,RecyclerVIew、Adapter、ViewHolder相信大家都很熟悉了,我们直接进入主题。


新建一个ItemTouchImpl类,继承自ItemTouchHelper


class ItemTouchHelperImpl(private val callback: Callback): ItemTouchHelper(callback)

不需要实现任何方法,ItemTouchHelper将工作代理给了Callback,所以我们接下来要实现这个Callback。


新建一个ItemTouchHelperCallback,继承自ItemTouchHelper.Callback,默认情况下,我们需要至少实现getMovementFlags()onMove()onSwiped() 三个方法。


在这个需求中,我们不需要滑动的效果,所以onSwiped()空实现就好了,同时让getMovementFlags()返回只允许上下拖拽的标志位就行。


如果我们直接在ItemTouchHelperCallback中实现相关逻辑,那么相当于这个Callback只会被用来处理上下拖拽的情况,是一个定制的Callback。下次遇上点别的场景,我们依然需要重新建个类去实现getMovementFlags(),太麻烦了,也不够通用。


为了方便后面的开发者,我决定把它做成一个通用的组件,对外暴露需要的接口,需要用到的时候只需要按需实现需要的接口就行了。


新建个ItemTouchDelegate接口,分别空实现onMove(),onSwiped(),uiOnSwiping(),uiOnDragging(),uiOnClearView(),其中getMovementFlags()我们默认实现,让ItemTouchHelper进支持上下方向的拖动、其他行为禁止,也即能满足我们的需求。


interface ItemTouchDelegate {
fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Array<Int> {
val layoutManager = recyclerView.layoutManager
var swipeFlag = 0
var dragFlag = 0
if (layoutManager is LinearLayoutManager) {
if (layoutManager.orientation == LinearLayoutManager.VERTICAL) {
swipeFlag = 0 // 不允许滑动
dragFlag = (UP or DOWN) // 允许上下拖拽
} else {
swipeFlag = 0
dragFlag = (LEFT or RIGHT) // 允许左右滑动
}
}

return arrayOf(dragFlag, swipeFlag)
}

fun onMove(srcPosition: Int, targetPosition:Int): Boolean = true

fun onSwiped(position: Int, direction: Int) {}

// 刚开始滑动时,需要进行的UI操作
fun uiOnSwiping(viewHolder: RecyclerView.ViewHolder?) {}

// 刚开始拖动时,需要进行的UI操作
fun uiOnDragging(viewHolder: RecyclerView.ViewHolder?) {}

// 用户释放与当前itemView的交互时,可在此方法进行UI的复原
fun uiOnClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {}
}

然后,新建一个ItemTouchHelperCallback,把ItemTouchDelegate作为参数传进构造方法内,具体看代码:


class ItemTouchHelperCallback(@NotNull val helperDelegate: ItemTouchDelegate): ItemTouchHelper.Callback() {
private var canDrag: Boolean? = null
private var canSwipe: Boolean? = null

fun setDragEnable(enable: Boolean) {
canDrag = enable
}

fun setSwipeEnable(enable: Boolean) {
canSwipe = enable
}

override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val flags = helperDelegate.getMovementFlags(recyclerView, viewHolder)
return if (flags != null && flags.size >= 2) {
makeMovementFlags(flags[0], flags[1])
} else makeMovementFlags(0, 0)
}

override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return helperDelegate.onMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
helperDelegate.onSwiped(viewHolder.bindingAdapterPosition, direction)
}

override fun isItemViewSwipeEnabled(): Boolean {
return canSwipe == true
}

override fun isLongPressDragEnabled(): Boolean {
return canDrag == true
}

/**
* 更新UI
*/
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when(actionState) {
ACTION_STATE_SWIPE -> {
helperDelegate.uiOnSwiping(viewHolder)
}
ACTION_STATE_DRAG -> {
helperDelegate.uiOnDragging(viewHolder)
}
}
}

/**
* 更新UI
*/
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
helperDelegate.uiOnClearView(recyclerView, viewHolder)
}
}

看代码应该就一目了然了,在onSelectedChanged()里根据actionState,将具体的事件分发给uiOnSwiping()和uiOnDragging(),同时让它默认不支持拖动和滑动,按业务需要打开。


最后修改下ItemTouchHelperImpl,将ItemTouchHelperCallback传进去。


class ItemTouchHelperImpl(private val callback: ItemTouchHelperCallback): ItemTouchHelper(callback) {

}

怎么使用


只需在recyclerView初始化后加这样一段代码


// 实现拖拽
val itemTouchCallback = ItemTouchHelperCallback(object : ItemTouchDelegate{

override fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
if (mData.size > 1 && srcPosition < mData.size && targetPosition < mData.size) {
// 更换数据源中的数据Item的位置
Collections.swap(mData, srcPosition, targetPosition);
// 更新UI中的Item的位置
mAdapter.notifyItemMoved(srcPosition, targetPosition);
return true
}
return false
}

override fun uiOnDragging(viewHolder: RecyclerView.ViewHolder?) {
viewHolder?.itemView?.setBackgroundColor(Color.parseColor("#22000000"))
}

override fun uiOnClearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
viewHolder.itemView.setBackgroundColor(Color.parseColor("#FFFFFF"))
}

})

val itemTouchHelper = ItemTouchHelperImpl(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(mRecycler)

我们只需要实现onMove(),在onMove()主要是更新数据源的位置,以及UI界面的位置,在uiOnDragging()和uiOnClearView()里对item进行高亮显示和复原。剩下的onSwiped()滑动那些不在需求范围内,不需要实现。


但还是不能用,还记得我们的helper是默认不支持滑动和滚动的吗,我们要使用的话,还需要打开开关,就可以实现本文开头那样的效果了


itemTouchCallback.setDragEnable(true) 

如果你需要支持滑动,只需要修改下重新实现getMovementFlags(),onSwiped(),同时设置setSwipeEnable() = true即可。


源码在这里,有需要的朋友麻烦自取哈


兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)



  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)

  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!

  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!


拜托拜托,谢谢各位同学!


作者:古嘉明同学
链接:https://juejin.cn/post/7110408776477310989
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Redis中的HotKey如何解决

对于三高系统,Redis是必须/必需的,当并发高到一定的程度就可能会出现HotKey的问题,今天我们来看下Redis中的HotKey如何解决。 什么是HotKey 在较短的时间内,海量请求访问一个Key,这样的Key就被称为HotKey。 HotKey的危害 ...
继续阅读 »

对于三高系统,Redis是必须/必需的,当并发高到一定的程度就可能会出现HotKey的问题,今天我们来看下Redis中的HotKey如何解决。


什么是HotKey


在较短的时间内,海量请求访问一个Key,这样的Key就被称为HotKey。


HotKey的危害



  • 海量请求在较短的时间内,访问一个Key,势必会导致被访问的Redis服务器压力剧增,可能会将Redis服务器击垮,从而影响线上业务;

  • HotKey过期的一瞬间,海量请求在较短的时间内,访问这个Key,因为Key过期了,这些请求会走到数据库,可能会将数据库击垮,从而影响线上业务。(这是缓存击穿问题)


HotKey如何解决


HotKey如何解决是一个比较宽泛的问题,涉及到多个方面,我们一个个来看。


Redis部署


通常来说,Redis有两种集群形式:数据分片集群、主从+哨兵集群,其实这两种集群形式或多或少的都一定程度上缓解了HotKey的问题。


主从+哨兵集群


如果我们采用单主:



  • 所有的读请求都会打在仅有的一个Redis服务器,都不用管Key是什么,只要并发一高,就会导致Redis服务器压力剧增;

  • 一旦仅有的一个Redis服务器挂了,就没有第二个Redis服务器顶上去了,无法继续提供服务。


如果我们采用主从+哨兵集群:



  • 读请求会被分散到Master节点或者多台Slave节点,将请求进行了初步的分散;

  • Master节点挂了,Slave节点会升级为新的Master节点,继续提供服务。


数据分片集群


Key被分散在了不同的Redis节点,将请求进行了进一步的分散。


如果采用数据分片集群,同时也会部署主从+哨兵,这样又有了主从+哨兵集群的特性:



  • 读请求会被分散到Master节点或者多台Slave节点,将请求进行了初步的分散;

  • Master节点挂了,Slave节点会升级为新的Master节点,继续提供服务。



画外音:我以前一直以为大部分公司都已经采用了数据分片集群,其实不然,某个我认为不差钱的公司,在2021年采用的还是主从+哨兵集群,出了问题,才转变成数据分片集群,我到我们公司一瞧,才发现我们公司也是主从+哨兵集群。



隔离


不同的业务分配不同的Redis集群,不要将所有的业务都“混杂”在一个Redis集群。


只要可以做到集群+隔离,在一定程度上就已经避免了HotKey,但是对于超高并发的系统来说,可能还有点不够,所以才会有下面的更进一步的措施。


如何应对HotKey


这个问题,可以拆分成三个子问题:如何发现HotKey、如何通知HotKey的产生、如何对HotKey进行处理。


如何发现HotKey


如何发现HotKey的前提是知道每个Key的使用情况,并进行统计,所以这又拆成了两个更小的子问题:如何知道每个Key的使用情况,如何进行统计。


如何知道每个Key的使用情况


谁最清楚知道每个Key的使用情况,当然是客户端、代理层,所以我们可以在客户端或者代理层进行埋点。


客户端埋点

在客户端请求Redis的代码中进行埋点。


优点:



  • 实现较为简单

  • 轻量级

  • 几乎没有性能损耗


缺点:



  • 进行统一管理较为麻烦:如果想开启或者关闭埋点、上报,会比较麻烦

  • 升级、迭代较为麻烦:如果埋点、上报方式需要优化,就需要升级Jar包,再找一个黄道吉日进行发布

  • 客户端会有一定的压力:不管是实时上报使用情况,还是准实时上报使用情况,都会对客户端造成一定的压力


代理层埋点

客户端不直接连接Redis集群,而是连接Redis代理,在代理层进行埋点。


优点:



  • 客户端没有压力

  • 对客户端完全透明

  • 升级、迭代比较简单

  • 进行统一管理比较简单


缺点:



  • 实现复杂

  • 会有一定的性能损耗:代理层需要转发请求到真正的Redis集群

  • 单点故障问题:需要做到高可用,更复杂

  • 单点热点问题:代理层本身就是一个热点,需要分散热点,更复杂


如何上报每个Key的使用情况


我们在客户端或者代理层进行了埋点,自然是由它们上报每个Key的使用情况,如何上报又是一个小话题。


实时/准实时


  • 实时上报:每次请求,都进行上报

  • 准实时上报:积累一定量或者一定时间的请求,再进行上报


是否预统计

如果采用准实时上报,在客户端或者代理层是否对使用情况进行预统计:



  • 进行预统计:减少上报的数据量,减轻统计的压力,自身会有压力

  • 不进行预统计:上报的数据量比较多,自身几乎没有压力


如何统计


不管如何进行上报,使用情况最终都会通过Kafka,发送到统计端,这个时候统计端就来活了。
一般来说,这个时候会借助于大数据,较为简单的方式:Flink开一个时间窗口,消费Kafka的数据,对时间窗口内的数据进行统计,如果在一个时间窗口内,某个Key的使用达了一定的阈值,就代表这是一个HotKey。


如何通知HotKey的产生


经过上面的步骤,我们已经知道了某个HotKey产生了,这个时候就需要通知到客户端或者代理层,那如何通知HotKey的产生呢?



  • MQ:用MQ通知客户端或者代理层HotKey是什么

  • RPC/Http:通过RPC/Http通知客户端或者代理层HotKey是什么

  • 配置中心/注册中心指令:既然遇到了HotKey的问题,并且想解决,那基本上是技术实力非常强大的公司,应该有非常完善的服务治理体系,此时,可以通过配置中心/注册中心下发指令到客户端或者代理层,告知HotKey是什么


如何处理HotKey


客户端或者代理层已经知晓了HotKey产生了,就自动开启一定的策略,来避免HotKey带来的热点问题:



  • 使用本地缓存,不至于让所有请求都打到Redis集群

  • 将HotKey的数据复制多份,分散到不同的Redis节点上


在实际开发中,可能在很大程度上,都不会有埋点、上报、统计,通知、策略自动开启,这一套比较完善的Redis HotKey解决方案,我们能做到的就是预估某个Key可能会成为热点,就采用本地缓存+复制多份HotKey数据的方式来避免HotKey带来的热点问题。我们还经常会因为偷懒,所以设计了一个大而全的Key,所有的业务都从这个Key中读取数据,但是有些业务只需要其中的一小部分数据,有些业务只需要另外一小部分数据,如果不同的业务读取不同的Key,又可以将请求进行分散,这是非常简单,而且有效的方式。


End


作者:CoderBear
链接:https://juejin.cn/post/7196393365314191417
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

对于单点登录,你不得不了解的CAS

大家好,我是车辙。之前我们通过面试的形式,讲了JWT实现单点登录(SSO)的设计思路,并且到最后也留下了疑问,什么是CAS。 没看过的同学建议点击下方链接先看下,两者还是有一定连贯性的。寒暄开始 今天是上班的第一天,刚进公司就见到了上次的面试官,穿着格子衬衫...
继续阅读 »

大家好,我是车辙。之前我们通过面试的形式,讲了JWT实现单点登录(SSO)的设计思路,并且到最后也留下了疑问,什么是CAS


没看过的同学建议点击下方链接先看下,两者还是有一定连贯性的。

寒暄开始



今天是上班的第一天,刚进公司就见到了上次的面试官,穿着格子衬衫和拖鞋,我们就叫他老余吧。老余看见我就开始勾肩搭背聊起来了,完全就是自来熟的样子,和我最近看的少年歌行里的某人很像。



什么是CAS呢


老余:上次你说到了CAS,你觉得CAS是什么?


我:之前我们面试的时候,我讲了JWT单点登录带来的问题,然后慢慢优化,最后衍变成了中心化单点登录系统,也就是CAS的方案。


CAS(Central Authentication Service),中心认证服务,就是单点登录的某种实现方案。你可以把它理解为它是一个登录中转站,通过SSO站点,既解决了Cookie跨域的问题,同时还通过SSO服务端实现了登录验证的中心化。



这里的SSO指的是:SSO系统



它的设计流程是怎样的


老余:你能不能讲下它的大致实现思路,这说的也太虚头巴脑了,简直是听君一席话,如听一席话。

我:你别急呀,先看下它的官方流程图。
image.png


重定向到SSO


首先,用户想要访问系统A的页面1,自然会调用http://www.chezhe1.com的限制接口,(比如说用户信息等接口登录后才能访问)。


接下来 系统A 服务端一般会在拦截器【也可以是过滤器,AOP 啥的】中根据Cookie中的SessionId判断用户是否已登录。如果未登录,则重定向到SSO系统的登录页面,并且带上自己的回调地址,便于用户在SSO系统登录成功后返回。此时回调地址是:http://www.sso.com?url=www.chezhe1.com



这个回调地址大家应该都不会陌生吧,像那种异步接口或者微信授权、支付都会涉及到这块内容。不是很了解的下面会解释~

另外这个回调地址还必须是前端页面地址,主要用于回调后和当前系统建立会话。



此时如下图所示:
image.png


用户登录



  1. 在重定向到SSO登录页后,需要在页面加载时调用接口,根据SessionId判断当前用户在SSO系统下是否已登录。【注意这时候已经在 SSO 系统的域名下了,也就意味着此时Cookie中的domain已经变成了sso.com



为什么又要判断是否登录?因为在 CAS 这个方案中,只有在SSO系统中为登录状态才能表明用户已登录。




  1. 如果未登录,展现账号密码框,让用户输入后进行SSO系统的登录。登录成功后,SSO页面和SSO服务端建立起了会话。
    此时流程图如下所示:


image.png


安全验证


老余:你这里有一个很大的漏洞你发现没有?

我:emm,我当然知道。


对于中心化系统,我们一般会分发对应的AppId,然后要求每个应用设置白名单域名。所以在这里我们还得验证AppId的有效性,白名单域名和回调地址域名是否匹配。否则有些人在回调地址上写个黄色网站那不是凉凉。


image.png

获取用户信息登录



  1. 在正常的系统中用户登录后,一般需要跳转到业务界面。但是在SSO系统登录后,需要跳转到原先的系统A,这个系统A地址怎么来?还记得重定向到SSO页面时带的回调地址吗?


image.png


通过这个回调地址,我们就能很轻易的在用户登录成功后,返回到原先的业务系统。



  1. 于是用户登录成功后根据回调地址,带上ticket,重定向回系统A,重定向地址为:http://www.chezhe1.com?ticket=123456a

  2. 接着根据ticket,从SSO服务端中获取Token。在此过程中,需要对ticket进行验证。

  3. 根据tokenSSO服务端中获取用户信息。在此过程中,需要对token进行验证。

  4. 获取用户信息后进行登录,至此系统A页面和系统A服务端建立起了会话,登录成功。


此时流程图如下所示:


image.png


别以为这么快就结束了哦,我这边提出几个问题,只有把这些想明白了,才算是真的清楚了。



  • 为什么需要 Ticket?

  • 验证 Ticket 需要验证哪些内容?

  • 为什么需要 Token?

  • 验证 Token 需要验证哪些内容?

  • 如果没有Token,我直接通过Ticket 获取用户信息是否可行?


为什么需要 Ticket


我们可以反着想,如果没有Ticket,我们该用哪种方式获取Token或者说用户信息?你又该怎么证明你已经登录成功?用Cookie吗,明显是不行的。


所以说,Ticket是一个凭证,是当前用户登录成功后的产物。没了它,你证明不了你自己。


验证 Ticket 需要验证哪些内容



  1. 签名:对于这种中心化系统,为了安全,绝大数接口请求都会有着验签机制,也就是验证这个数据是否被篡改。至于验签的具体实现,五花八门都有。

  2. 真实性:验签成功后拿到Ticket,需要验证Ticket是否是真实存在的,不能说随便造一个我就给你返回Token吧。

  3. 使用次数:为了安全性,Ticket只能使用一次,否则就报错,因为Ticket很多情况下是拼接在URL上的,肉眼可见。

  4. 有效期:另外则是Ticket的时效,超过一定时间内,这个Ticket会过期。比如微信授权的Code只有5分钟的有效期。

  5. ......


为什么需要 Token?


只有通过Token我们才能从SSO系统中获取用户信息,但是为什么需要Token呢?我直接通过Ticket获取用户信息不行吗?


答案当然是不行的,首先为了保证安全性Ticket只能使用一次,另外Ticket具有时效性。但这与某些系统的业务存在一定冲突。因此通过使用Token增加有效时间,同时保证重复使用。


验证 Token 需要验证哪些内容?


和验证 Ticket类似



  1. 签名 2. 真实性 3. 有效期


如果没有 Token,我直接通过 Ticket 获取用户信息是否可行?


这个内容其实上面已经给出答案了,从实现上是可行的,从设计上不应该,因为TicketToken的职责不一样,Ticket 是登录成功的票据,Token是获取用户信息的票据。


用户登录系统B流程


老余:系统A登录成功后,那系统B的流程呢?

我:那就更简单了。


比如说此时用户想要访问系统B,http://www.chezhe2.com的限制接口,系统B服务端一般会在拦截器【也可以是过滤器,AOP 啥的】中根据Cookie中的SessionId判断用户是否已登录。此时在系统B中该系统肯定未登录,于是重定向到SSO系统的登录页面,并且带上自己的回调地址,便于用户在SSO系统登录成功后返回。回调地址是:http://www.sso.com?url=www.chezhe2.com。


我们知道之前SSO页面已经与SSO服务端建立了会话,并且因为CookieSSO这个域名下是共享的,所以此时SSO系统会判断当前用户已登录。然后就是之前的那一套逻辑了。
此时流程图如下所示:


image.png


技术以外的事


老余:不错不错,理解的还可以。你发现这套系统里,做的最多的是什么,有什么技术之外的感悟没。说到这,老余叹了口气。


我:我懂,做的最多的就是验证了,验证真实性、有效性、白名单这些。明明一件很简单的事,最后搞的那么复杂。像现在银行里取钱一样,各种条条框框的限制。我有时候会在想,技术发展、思想变革对于人类文明毋庸置疑是有益的,但是对于我们人类真的是一件好事吗?如果我们人类全是机器人那样的思维是不是会更好点?


image.png

老余:我就随便一提,你咋巴拉巴拉了这么多。我只清楚一点,拥有七情六欲的人总是好过没有情感的机器人的。好了,干活去吧。


总结


这一篇内容就到这了,我们聊了下关于单点登录的 CAS 设计思路,其实CAS 往大了讲还能讲很多,可惜我的技术储备还不够,以后有机会补充。如果想理解的更深刻,也可以去看下微信授权流程,应该会有帮助。


最后还顺便提了点技术之外的事,记得有句话叫做:科学的尽头是哲学,我好像开始慢慢理解这句话的意思了。


作者:车辙cz
链接:https://juejin.cn/post/7196924295310262328
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Compose自定义View——LocationMarkerView

LocationMarker是运动轨迹上Start、End, 以及整公里点上笔者自定义绘制的一个MarkerView, 当时之所以没有用设计给的icon是这个MarkerView里需要填充动态的数字,自定义的话自主性比较大些也方面做动画,之前的Android ...
继续阅读 »

LocationMarker是运动轨迹上Start、End, 以及整公里点上笔者自定义绘制的一个MarkerView, 当时之所以没有用设计给的icon是这个MarkerView里需要填充动态的数字,自定义的话自主性比较大些也方面做动画,之前的Android 传统自定义View的实现可以看这篇文章介绍 运动App自定义LocationMarker


这里先看下gif动图:


LocationMarker.gif


LocationMarkerView图的绘制


绘制方面基本没有太多的逻辑,通过Compose的自定义绘制Canvas() 绘制 一个构建的Path,生成View的Path其实是主要的实现过程。


Canvas(modifier = Modifier.size(0.dp)){
 drawPath(AndroidPath(markerViewPath), color = color)
 drawPath(AndroidPath(bottomOval), color = colorOval)
}

这里Compose的path,还有好些接口对不上以及缺少API,所以通过AndroidPath(nativepath)接口进行转化进行绘制,bottomOval是 Start、End点底部阴影的Path。生成markerViewPath以及bottomOval的逻辑都在LocationMarker类中,LocationMarker主要包含了上下两套点 p1、p3(HPoint), 左右两套点p2、p4(VPoint), 以及绘制View的参数属性集合类MarkerParams.


获取markerViewPath, 首先给p1、p3(HPoint),p2、p4(VPoint)中8个点设置Value值,circleModel(radius),然后从底部p1底部点逆时针转圈依次调用三阶贝塞尔函数接口,最后close实现水滴倒置状态的Path,见实现:


fun getPath(radius: Float): Path{
 circleModel(radius)
 val path = Path()
 p1.setYValue(p1.y + radius * 0.2f * 1.05f) //设置 p1 底部左右两个点的y值
 p1.y += radius * 0.2f * 1.05f //设置 p1 自己的y值
 path.moveTo(p1.x, p1.y)
 path.cubicTo(p1.right.x, p1.right.y, p2.bottom.x, p2.bottom.y, p2.x, p2.y)
 path.cubicTo(p2.top.x, p2.top.y, p3.right.x, p3.right.y, p3.x, p3.y)
 path.cubicTo(p3.left.x, p3.left.y, p4.top.x, p4.top.y, p4.x, p4.y)
 path.cubicTo(p4.bottom.x, p4.bottom.y, p1.left.x, p1.left.y, p1.x, p1.y)
 path.close()
 val circle = Path()
 circle.addCircle(p3.x, p3.y + radius, markerParams.circleRadius.value, Path.Direction.CCW)
 path.op(circle, Path.Op.DIFFERENCE)
 return path
}

拿到相应的Path后,在Composeable函数里进行如上所述的绘制Path即可:


val locationMarker = LocationMarker(markerParams)
val markerViewPath = locationMarker.getPath(markerParams.radius.value)
val bottomOval = locationMarker.getBottomOval()
val color = colorResource(id = markerParams.wrapperColor)
val colorOval = colorResource(R.color.location_bottom_shader)

Canvas(modifier = Modifier.size(0.dp)){
 drawPath(AndroidPath(markerViewPath), color = color)
 drawPath(AndroidPath(bottomOval), color = colorOval)
}

绘制整公里的文字


Compose的Canvas 里目前的Version并不支持drawText的绘制,不过开放了一个调用原始drawText的转换API, 原始的drawText 是需要Paint参数的, 同时依赖Paint来计算Text 对应RectF的Height值,这里Paint()是Compose的一个Paint,需要调用asFrameworkPaint() 进行转化


val paint = Paint().asFrameworkPaint().apply {
 setColor(-0x1)
 style = android.graphics.Paint.Style.FILL
 strokeWidth = 1f
 isAntiAlias = true
 typeface = Typeface.DEFAULT_BOLD
 textSize = markerParams.txtSize.toFloat()
}

计算Text 绘制依赖的RectF,并将rectF.left作为drawText的X值,同时计算drawText的基线 baseLineY,最后传入nativeCanvas.drawText() 接口进行绘制。


val rectF = createTextRectF(locationMarker, paint, markerParams)
val baseLineY = getTextBaseY(rectF, paint)
Canvas(modifier = Modifier.size(0.dp)){
 drawIntoCanvas {
   it.nativeCanvas.drawText(markerParams.markerStr,  rectF.left, baseLineY, paint)
}
}

drawText获取绘制基线 baseLineY的工具类方法:


fun getTextBaseY(rectF: RectF, paint: Paint): Float {
   val fontMetrics = paint.fontMetrics
   return rectF.centerY() - fontMetrics.top / 2 - fontMetrics.bottom / 2
}

添加动画


这里简单的用一个放大的动画实现,跟原始的高德地图、Mapbox地图的一个growth过程的一个动画有些差距的,暂且先这样实现吧。首先是定义两个radius相关的State对象,具体来说是Proxy, 以及一个动画生长的大小控制的Float的变量Fraction,再通过自定义animateDpAsState作为 animation值的对象,最终给到MarkParams作为参数,animation值的变化,会导致MarkParams的变化,最后导致Recompose,形成动画。


  val circleRadius by rememberSaveable{ mutableStateOf(25) }
 val radius by rememberSaveable{ mutableStateOf(60) }
 var animatedFloatFraction by remember { mutableStateOf(0f) }
 val radiusDp by animateDpAsState(
   targetValue = (radius * animatedFloatFraction).dp,
   animationSpec = tween(
     durationMillis = 1000,
     delayMillis = 500,
     easing = LinearOutSlowInEasing
  )
)

 val circleRadiusDp by animateDpAsState(
   targetValue = (circleRadius * animatedFloatFraction).dp,
   animationSpec = tween(
     durationMillis = 1000,
     delayMillis = 500,
     easing = LinearOutSlowInEasing
  )
)

 val markerParams by remember {
   derivedStateOf { MarkerParams(radiusDp, circleRadiusDp, wrapperColor = wrapperColor) }
}
 

Compose 自定义View LocationMarkerView 主要通过drawPath,以及调用原生的drawText, 最后添加了一个scale类似的动画实现,最终实现运动轨迹里的一个小小的View的实现。


代码见:github.com/yinxiucheng… 下的CustomerComposeView


作者:cxy107750
链接:https://juejin.cn/post/7197433837340901432
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在Android中实现python的功能

起因:为什么想写这样一篇文章呢,最开始是我的一个朋友和我说想换一个QQ头像但是苦于里面一个常用的模块被下架了在网页中找无关信息太多也显得杂乱,我就萌生了这样一个想法,我是一个程序员这种事能不能通过技术手段实现或者说简化一下说干咱就干(PS:目前只通过Java实...
继续阅读 »

起因:

为什么想写这样一篇文章呢,最开始是我的一个朋友和我说想换一个QQ头像但是苦于里面一个常用的模块被下架了在网页中找无关信息太多也显得杂乱,我就萌生了这样一个想法,我是一个程序员这种事能不能通过技术手段实现或者说简化一下说干咱就干

(PS:目前只通过Java实现了爬虫的功能就不多赘述了具体的可以自行百度,python的部分并未能全部实现故只介绍前期的准备流程及部分结果)

需要准备的工具:

Android Studio,adaconda

接下来让我们开始吧!

  1. 首先为了能在as中创建python文件我们需要先下载一个插件。在Plugins中搜索Python Community Edition插件下载,安装重启as后就可以在as中创建python文件了,因为Chaquopy没有与这个插件集成,所以.py文件中的代码会报错这是正常现象可以忽略,实际错误请以logcat为准
  2. 打开根目录下build.gradle文件引入chaquo模块
buildscript {
repositories {
xxx
maven { url 'https://jitpack.io' }
//引入chaquo模块
maven { url "https://chaquo.com/maven" }

}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
//如果该模块的版本引入不对会引起编译失败
//如果这里使用的版本是12.0.0及更早的版本会在模块启动时弹出吐司及通知栏显示许可证警告,并且一次只能运行五分钟
//想要删除限制需要在local.properties文件中引入chaquopy.license = free12.0.1及之后的版本则为开源的无需额外配置
classpath "com.chaquo.python:gradle:12.0.1"
}
}

local.properties文件中内容如下

#使用闭源 Chaquopy 版本(12.0.0 及更早版本)将在启动时显示许可证警告,并且一次只能运行 5 分钟。要删除这些限制,请将以下内容添加到您的项目.
#chaquopy.license=free
#如果使用闭源代码的Chapuopy版本来构建AAR,还需要增添如下标识将AAR内置到应用程序中
#chaquopy.applicationId=your.applicationId

3.接下来让我们打开app目录下的build.gradle文件加入以下引用

plugins {
//应用模块
id 'com.android.application'
id 'com.chaquo.python'
}
android {
ndk {
//引入python模块后不支持架构为armeabicpu类型
abiFilters 'armeabi-v7a', 'arm64-v8a', "x86", "x86_64"
// 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
}

python {
//adaconda中的python编译器,目的引入虚拟环境让python文件在安卓应用中运行,buildPython中的路径需要替换为你自己的安装地址
buildPython "D:\\ana_2\\python.exe"
pip {
//指定库的镜像下载地址:阿里云,清华等
//options "--index-url", "https://mirrors.aliyun.com/pypi/simple/"
options "--extra-index-url", "https://pypi.tuna.tsinghua.edu.cn/simple/"
//install "opencv-python"
//下载的库,需要什么模块就自行下载下载什么模块,另有些模块不支持引入详情请参阅https://chaquo.com/chaquopy/doc/current/android.html#stdlib-unsupported
install "requests"
}
}
}

4.完成以上配置后就可以开始真正的旅程了

//初始化python模块的相关文件
void initPython() {
if (!Python.isStarted()) {
Python.start(new AndroidPlatform(this));
}
}

//调用python中的内容
void callPythonCode() {
Python py = Python.getInstance();
//getModule:py文件名,不用加.py的后缀;callAttr:方法名;如果方法有返回值那pyObject就是返回值
PyObject pyObject = py.getModule("SearchHeadImg").callAttr("sjs");
String a = String.valueOf(pyObject);
Log.e(".py返回值", a);
}

这样我们就可以在app中调用python的相关功能了!

这些内容虽说不多但也是我花了很长时间踩坑一步一步总结出来的,如果有问题或者缺失的内容欢迎大佬指正补充。

收起阅读 »

CSS简单实现一幅新春对联

web
前言今年过年家里没有贴春联,这两天在网上看到一幅对联,觉得写得挺好的, 上联——只生欢喜不生愁;下联——此心安处是吾家; 横批——平安喜乐, 因此使用css简单实现这幅新春对联。具体实现页面先做一个简单描述,首先页面中间有一个大门,然后在大门两侧实现春联的上下...
继续阅读 »

前言

今年过年家里没有贴春联,这两天在网上看到一幅对联,觉得写得挺好的, 上联——只生欢喜不生愁;下联——此心安处是吾家; 横批——平安喜乐, 因此使用css简单实现这幅新春对联。

具体实现

页面先做一个简单描述,首先页面中间有一个大门,然后在大门两侧实现春联的上下联,大门的上面实现春联的横批,再做一个打开大门,出现兔年祝福图片的效果。

效果展示:(毛笔字体文件没有线上的资源,所以字体没有效果) code.juejin.cn/pen/7197022…

页面整体布局:

<div class="wrapper">
   <div class="container">
     <div class="title">平安喜乐</div>
     <div class="content">
       <h1>此心安处是吾家</h1>
       <div class="door">
         <div class="door-l"></div>
         <div class="door-r"></div>
         <!-- 送福图片 -->
         <img src="/4034970a304e251fb44609698ce95a1c7e3e536c.webp" alt="" class="pic">
       </div>
       <h1>只生欢喜不生愁</h1>
     </div>
   </div>
 </div>

1. 大门的实现

大门的总体宽高都设置成350px,设置视角(perspective:1000px), 大门打开的时候呈现一种3D的视觉感受。

大门分成左右两部分门扇,使用绝对定位控制左右的位置,并使用transform-origin属性设置大门旋转动画的基点,默认情况下,元素的动作参考点(基点)为元素盒子的中心(center),这里设置左边门扇的transform-origin: left,左门扇以左边基点旋转;右边门扇的transform-origin: right,右门扇以右边基点旋转。

大门门扇的圆形门环使用伪元素实现,使用hover属性实现当鼠标移到大门上时,大门的门扇分别旋转一定的角度,实现打开大门的效果

兔年祝福图片使用绝对定位控制在大门的居中位置,并设置层级最低,当打开大门图片慢慢变大

.door {
 width: 350px;
 height: 350px;
 border: 2px solid #333;
 margin: 0 auto;
 position: relative;
 perspective: 1000px;
}
.door .pic{
 position: absolute;
 top: 50%;
 left: 50%;
 width: 70%;
 object-fit: cover;
 transform: translate(-50%,-50%);
 z-index: -1;
 transition: all 0.3s ease-in;
}
.door-l,
.door-r {
 width: 50%;
 height: 100%;
 background-color: #e1b12c;
 position: absolute;
 top: 0;
 transition: all 0.5s;
}

.door-l {
 left: 0;
 border-right: 1px solid #000;
 transform-origin: left;
}

.door-r {
 right: 0;
 border-left: 1px solid #000;
 transform-origin: right;
}

.door-l::before,
.door-r::before {
 content: "";
 border: 1px solid #000;
 width: 20px;
 height: 20px;
 position: absolute;
 top: 50%;
 border-radius: 50%;
 transform: translateY(-50%);
}

.door-l::before {
 right: 5px;
}

.door-r::before {
 left: 5px;
}

.door:hover .door-l {
 transform: rotateY(-120deg);
}

.door:hover .door-r {
 transform: rotateY(120deg);
}
.door:hover .pic{
 width: 100%;
}

2. 春联的实现

春联一般是用毛笔写的,因此在网上找了一款毛笔字体下载下来,并引入到样式中,并给春联设置红色的背景

网上下载下来的毛笔字体为trueType格式(.ttf,Windows和Mac上常见的字体格式,是一种原始格式,没有为网页进行优化处理),需要转换成Web Open Font格式(.woff,针对网页进行特殊优化,是Web字体中最佳格式)。可以在这个网站上传字体进行转换

@font-face 用于设置自定义字体,可以自定义字体名称。两个必要属性:

  • font-family:给引入的字体起一个名称,注意:名字不要和那些专属的名称起冲突了,比如:微软雅黑。

  • src:自定义字体的路径,一般采用相对路径去使用。

@font-face {
 font-family: 'YFJLXS8';
 src: url('./font.woff2') format('woff2'),
   url('./font.woff') format('woff');
 font-weight: normal;
 font-style: normal;
}
* {
 margin: 0;
 padding: 0;
 box-sizing: border-box
}
.wrapper {
 height: 100vh;
 font-family: 'YFJLXS8', 'Courier New', Courier, monospace;
 padding: 50px;
 overflow: hidden;
 background: #ccc;
}
.content {
 display: flex;
 align-items: center;
 justify-content: center;
 width: 44%;
 margin: 20px auto;
}
h1 {
 font-size: 40px;
 font-weight: 700;
 width: 5vw;
 color: #000;
 line-height: 1;
 text-align: center;
 background-color: #d63031;
 padding: 20px 0;
}
.title{
 width: 20%;
 font-size: 40px;
 font-weight: 700;
 text-align: center;
 margin: 0 auto;
 background-color: #d63031;
}

作者:sherlockkid7
来源:juejin.cn/post/7196994373237866553

收起阅读 »

详解css中伪元素::before和::after和创意用法

web
伪类和伪元素首先我们需要搞懂两个概念,伪类和伪元素,像我这种没有系统全面性的了解过css的人来说,突然一问我伪类和伪元素的区别我还真不知道,我之前一直以为这两个说法指的是一个东西,就是我题目中的提到的那两个::before和::after。偶然间才了解到,原来...
继续阅读 »

伪类和伪元素

首先我们需要搞懂两个概念,伪类和伪元素,像我这种没有系统全面性的了解过css的人来说,突然一问我伪类和伪元素的区别我还真不知道,我之前一直以为这两个说法指的是一个东西,就是我题目中的提到的那两个::before::after。偶然间才了解到,原来指的是两个东西

伪类

w3cSchool对于伪类的定义是”伪类用于定义元素的特殊状态“。向我们常用到的:link:hover:active:first-child等都是伪类,全部伪类比较多,大家感兴趣的话可以去官方文档了解一下

伪元素

至于伪元素,w3cSchool的定义是”CSS 伪元素用于设置元素指定部分的样式“,光看定义我是搞不懂,其实我们只要记住有哪些东西就好了,伪元素共有5个,分别是::before::after::first-letter::first-line::selection

伪类和伪元素可以叠加使用,如.sbu-btn:hover::before,本文后面示例部分也会用到此种用法。

::first-letter主要用于为文本的首字母添加特殊样式

注意:::first-letter 伪元素只适用于块级元素。

::first-line 伪元素用于向文本的首行添加特殊样式。

注意:::first-line 伪元素只能应用于块级元素。

::selection 伪元素匹配用户选择的元素部分。也就是给我们鼠标滑动选中的部分设置样式,它可以设置以下属性

  • color

  • background

  • cursor

  • outline

以上几种我们简单了解一下就可以了,也不在我们今天的讨论范围之内,今天我们来着重了解一下::before::after,相信大家在工作中都或多或少的用过,但很少有人真的去深入的了解过他们,本文是我对我所知的关于他们用法的一个总结,如有缺漏,欢迎补充。

用法及示例

::before用于在元素内容之前插入一些内容,::after用于在元素内容之后插入一些内容,其他方面的都相同。写法就是只要在想要添加的元素选择器后面加上::before::after即可,有些人会发现,写一个冒号和两个冒号都可以有相应的效果,那是因为在css3中,w3c为了区分伪类和伪元素,用双冒号取代了伪元素的单冒号表示法,所以我们以后在写伪元素的时候尽量使用双冒号。

不同于其他伪元素,::before::after在使用的时候必须提供content属性,可以为字符串和图片,也可以是空,但不能省略该属性,否则将不生效。

给指定元素前添加内容

这个用法是最基础也是最常用的,比如我们可以给一个或多个元素前面或者后面添加想要的文字

  <div class="class1">
   <p class="q">你的名字是?</p>
   <p class="a">张三</p>
   <p class="q">你的名字是?</p>
   <p class="a">张三</p>
   <p class="q">你的名字是?</p>
   <p class="a">张三</p>
 </div>
    .class1::before {
    content: '问卷';
    font-size: 30px;
  }

  .class1 .q::before {
    content: '问题:'
  }

  .class1 .a::before {
    content: '回答:'
  }


当然也可以添加形状,默认的是行内元素,如果有需要,我们可以把它变为块级元素

  <div class="class2">
   <div class="news-item">今天天气为多云</div>
   <div class="news-item">今天天气为多云</div>
   <div class="news-item">今天天气为多云</div>
   <div class="news-item">今天天气为多云</div>
   <div class="news-item">今天天气为多云</div>
 </div>
  .news-item::before {
    content: '';
    display: inline-block;
    width: 16px;
    height: 16px;
    background: rgb(96, 228, 255);
    margin-right: 8px;
    border-radius: 50%;
  }


我们也可以使用它来添加图片

  <div class="class3">
   <p class="text1">阅读和写作同样重要</p>
   <p class="text1">阅读和写作同样重要</p>
   <p class="text1">阅读和写作同样重要</p>
   <p class="text1">阅读和写作同样重要</p>
 </div>
  .class3 .text1::before {
    content: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg);
  }

不过这一方法的缺点就是,不能调整图片大小,如果我们需要使用伪元素添加图片的话,建议通过给伪元素设置背景图片的方式设置

结合clear属性清除浮动

我们都知道清除浮动的一种方式就是给一个空元素设置clear:both属性,但在页面里添加过多的空元素一方面代码不够简洁,另一方面也不便于维护,所以我们可以通过给伪元素设置clear:both属性的方法更好的实现我们想要的效果

禁用网页ctrl+f搜索

有些时候,我们不想要用户使用ctrl+f搜索我们网页内的内容,必须在一些文字识别的网页小游戏里,我们又不想把文字做成图片,那么就可以使用这个属性,使用::before::after渲染出来的文字,不可选中也不能搜索。当然这个低版本浏览器的兼容性我木有试,谷歌浏览器和safari是可以实现不能选中不可搜索的效果的。

拿上面的示例进行尝试,可以看到,我们使用伪元素添加的[问题]两个字,就无法使用浏览器的搜索工具搜到。


制作一款特殊的鼠标滑入滑出效果

这个效果还是之前一个朋友从某网站看到之后问我能不能实现,我去那个网站查看了代码学会的,觉得很有趣,特意分享给大家。

可以先看一下效果


这里附上源码和在线演示


    .h-button {
     z-index: 1;
     position: relative;
     overflow: hidden;
  }

   .h-button::before,
   .h-button::after {
     content: "";
     width: 0;
     height: 100%;
     position: absolute;
     filter: brightness(.9);
     background-color: inherit;
     z-index: -1;
  }

   .h-button::before {
     left: 0;
  }

   .h-button:after {
     right: 0;
     transition: width .5s ease;
  }

   .h-button:hover::before {
     width: 100%;
     transition: width .5s ease;
  }

   .h-button:hover::after {
     width: 100%;
     background-color: transparent;
  }

这里我做了一些改进,就是鼠标滑入之后的颜色是对按钮本身颜色进行一定的变换得来的,这样我们就无需对每一个按钮单独设置鼠标滑入时候的颜色了,全局时候的时候只需要对目标按钮添加一个类名h-button就可以,更加的方便简单,当然,如果大家觉得这样的颜色不好看的话,还是可以自行设置,或者修改一我对颜色的处理方式

这个效果的实现思路其实很简单,就是使用::before::after给目标按钮添加两个伪元素,然后使用定位让他们重合在一起,再通过改变两者的宽度实现的。

首先是创建两个伪元素,宽高都和目标元素一致,我这里的背景色由于是对按钮本身颜色进行处理得来的,所以给他们设置的背景色是沿用父级背景色,如果你想单独设置这里可以分别设置为自己想要的颜色。

    .h-button {
     z-index: 1;
     position: relative;
     overflow: hidden;
  }

   .h-button::before,
   .h-button::after {
     content: "";
     width: 0;
     height: 100%;
     position: absolute;
     filter: brightness(.9);
     background-color: inherit;
     z-index: -1;
  }

我们的实现原理是通过改变伪元素的宽度实现,所以我们需要第一个伪元素的定位以左边为准,从而实现鼠标移入时色块从左往右出现的效果,而第二个伪元素的定位以右为准,从而实现鼠标移出时色块从左往右消失的效果。

这里可以看到,我们在没有给第一个伪元素的初始状态添加过渡效果,那是因为它只需要在从鼠标移出的时候展示动画即可,在鼠标移出的时候需要瞬间消失,所以在初始状态不需要添加过渡效果,而第二个伪元素恰恰相反,它在鼠标滑入的时候不需要展示动画效果,在鼠标滑入也就是回归初始状态的时候需要展示动画效果,所以我们需要在最开始的时候就添加上过渡效果。

    .h-button::before {
     left: 0;
  }

   .h-button::after {
     right: 0;
     transition: width .5s ease;
  }

两个伪元素的初始宽度都为0,鼠标滑入的时候,让两个伪元素宽度都变为100%,由于鼠标滑入时我们并不需要第二个伪元素出现,所以这里我们给它的背景颜色设置为透明,这样就可以实现鼠标滑入时只展示第一个伪元素宽度从0到100%的动画,而鼠标移出时第一个伪元素宽度变为0,因为没有过渡效果,所以它的宽度会瞬间变为0,然后展示第二个色块宽度从100%到0的动画效果。

    .h-button:hover::before {
     width: 100%;
     transition: width .5s ease;
  }

  .h-button:hover::after {
     width: 100%;
     background-color: transparent;
  }

伪元素能实现的创意用法还有很多,如果大家有不同的用法,欢迎分享,希望本篇文章可以对大家有所帮助。

作者:十里青山
来源:juejin.cn/post/7163867155639828488

收起阅读 »

团队的技术分享又轮到我了,分享点啥才能显得牛逼又有趣?

web
引言新年好,我是飞叶_程序员。见过我这个ID的朋友们肯定都知道,作为前端,我主要通过 B站up主 的身份来来进行社区交流的。 虽然主要的交流渠道不是掘金、segmentfault这样的技术站点,但与在掘金活跃的大佬们遇到的问题其实是一样的。那就是我们需要经常阅...
继续阅读 »

引言

新年好,我是飞叶_程序员。

见过我这个ID的朋友们肯定都知道,作为前端,我主要通过 B站up主 的身份来来进行社区交流的。 虽然主要的交流渠道不是掘金、segmentfault这样的技术站点,但与在掘金活跃的大佬们遇到的问题其实是一样的。

那就是我们需要经常阅读技术文章、技术资讯,保持和丰富自己的知识储备,不然怎么给别人分享知识呢? 这是我作为一个创作者和分享者 和 广大其他创作者们遇到的共性问题

那作为一线的开发者,其实也有技术分享的需要,我相信大家的技术团队都是需要技术分享的。 而技术分享一般都是通过轮流进行的,也不能逮着团队里的几个人一直薅羊毛对吧。

那轮到你技术分享的时候,你是否会苦恼于不知道该分享点啥呢?
你是否担心:万一我分享的东西其他人都已经知道了,显得自己不够牛逼呢?

我想这些问题,归根到底是不知道去哪里获取技术资讯的问题。
如果你手里有大量的技术站点,他们能给你提供大量的高质量技术文章,在里面找到一篇值得分享的内容应该就不难了。

回顾2022年,我在B站发布了100多个技术视频,平均约每周两个,现在看起来都不可思议。 哪有那么多可以分享的内容啊!

前端森林

实际上我能分享那么多,得益于我收录了一些英文站点。尤其是有一些技术周刊。
我的灵感来源都是他们。不是凭空产生的。

过年期间我一直在想着把我收藏的这些站点公开出来,让其他人和创作者们也不再有技术分享的苦恼。 所以创建了一个开源项目,叫awesome-fe-sites,GitHub, 并把它部署在了fesites.netlify.app

他的作用是收录前端资讯类站点,周刊类网站,高质量个人博客和技术团队博客,在线服务类/工具类网站等。
slogan:前端网站,尽收眼底。

同时也希望它也可以解放你的浏览器书签栏。

参与贡献

不知道你有没有一些私藏的高质量的前端站点,如果你希望把它贡献出来,欢迎PR。

另外这个站点是通过qwik这个很新的前端框架搭建的,对qwik感兴趣的话,也可以看看这个项目的代码。

作者:飞叶_前端
来源:juejin.cn/post/7193136620948684860

收起阅读 »

入坑两个月自研创业公司

一、拿offer其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果一...
继续阅读 »

一、拿offer

其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果一直拖到7月。

二、入职工作

刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变

三、人言可畏

刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……

四、为什么离开

最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。

五、收获

1.不要脱产,不要脱产 2.使用uniapp进行微信和支付宝小程序开发 3.工作离家近真的很爽 4.作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。”问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。 5.进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…

六、未来规划

关于下一份工作: 南京真是外包之城,找了两周只有外包能满足我目前18k的薪资,还有一家还降价了500… 目前offer有 vivo外包,20k 美的外包,17.5k 自研中小企业,18.5k

虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点: 1.全球手机出货量下降,南京的华为外包被裁了不少,很难说以后vivo会不会也裁。 2.美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展b2c业务,我进去做的也是和商场相关。 3.美的的办公地点离我家更近些 4.自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。

关于考公: 每年10月到12月准备下,能进就进,不能再在考公上花费太多时间了。

作者:哇哦谢谢你
来源:juejin.cn/post/7160138475688165389

收起阅读 »

不修改任何现有源代码,将项目从 webpack 迁移到 vite

web
背景 之前将公司项目开发环境从 webpack 迁移到 vite,实现了 dev 环境下使用 vite、打包使用 webpack 的共存方案。本文将讲述开发环境下 vue3 项目打包器从 webpack 迁移到 vite 过程中的所遇问题、解决方案、迁移感受,...
继续阅读 »

背景


之前将公司项目开发环境从 webpack 迁移到 vite,实现了 dev 环境下使用 vite、打包使用 webpack 的共存方案。本文将讲述开发环境下 vue3 项目打包器从 webpack 迁移到 vite 过程中的所遇问题、解决方案、迁移感受,以及如何不修改任何源码完成迁移。


迁移的前提及目标


我们之前的项目大概有 10w+ 行代码,开发环境下冷启动所花费的时间大概 1 分钟多,所以迁移到 vite 就是看中了它的核心价值:快!但是迁移到 vite,也会伴随着风险:代码改动及回归成本。


作为一个大型的已上线项目,它的线上稳定性的一定比我们工程师开发时多减少一些项目启动时间的价值要高,所以如果迁移带来了很多线上问题,那便得不偿失了。


所以我们迁移过程中有前提也有目标:



  • 前提:不因为迁移打包工具引发线上问题

  • 目标:实现开发环境下的快速启动


方案


有了上述前提和目标,那我们的方案就可以从这两方面思考入手了。



  • 如何能确保实现前提?我们已有了稳定版本,那只要保证源代码不改动,线上的打包工具 webpack 及配置也不改动,就可以确保实现前提。

  • 如何实现目标?vite 的快主要是体现在开发环境,打包使用的 rollup 相比 webpack 速度上并无太明显的优势,所以我们只要开发环境下使用 vite 启动就可以实现目标。


由此得出最终方案:不改动任何现有源代码,开发环境使用 vite,线上打包使用 webpack。


迁移过程


安装 vite 及进行基础配置



  • 在终端执行下述命令,安装 vite 相关基础依赖:
    yarn add vite @vitejs/plugin-vue vite-plugin-html -D
    复制代码


  • 因为 vite 的 html 模板文件需要显示引入入口的 .js/.ts 文件,同时有一些模板变量上面的区别,为了完全不影响线上打包,在 /public 目录下新建一个 index.vite.html 文件。将 /public/index.html 文件的内容拷贝进来并添加入口文件的引用( /src/main.ts 指向项目的入口文件):
    <!DOCTYPE html>
    <html lang="">
    <!-- other code... -->
    <body>
    <!-- other code... -->
    <div id="app"></div>
    + <script type="module" src="/src/main.ts"></script>
    </body>
    </html>
    复制代码


  • 新增 vite.config.js,内容如下:
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import { createHtmlPlugin } from 'vite-plugin-html';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [
    vue(),
    createHtmlPlugin({
    minify: true,
    /**
    * After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
    * @default src/main.ts
    */
    entry: 'src/main.ts',
    /**
    * If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
    * @default index.html
    */
    template: 'public/index.vite.html',
    }),
    ]
    });
    复制代码


  • package.jsonscripts 里新增一条 vite 开发启动的指令:
    {
    "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    + "vite": "vite"
    }
    }
    复制代码



到这里,我们基本的配置就已经完成了,现在可以通过 npm run vite 来启动 vite 开发环境了,只不过会有一大堆的报错,我们根据可能遇到的问题一个个去解决。


问题及解决方案


HtmlWebpackPlugin 变量处理


报错htmlWebpackPlugin is not defined
image.png


是因为之前在 webpack 的 HtmlWebpackPlugin 插件中配置了变量,而 vite 中没有这个插件,所以缺少这个变量。


我们先前安装了 vite-plugin-html 插件,所以可以在这个插件中配置变量来代替:



  • index.vite.html 中所有的 htmlWebpackPlugin.options.xxx 修改为 xxx,如:
    <!DOCTYPE html>
    <html lang="">
    <head>
    - <title><%= htmlWebpackPlugin.options.title %></title>
    + <title><%= title %></title>
    </head>
    </html>

    复制代码


  • vite.config.js 中添加如下内容:
    export default defineConfig({
    plugins: [
    createHtmlPlugin({
    + inject: {
    + data: {
    + title: '我的项目',
    + },
    + },
    }),
    ]
    });
    复制代码



其他的 html 中未定义的变量亦可以通过此方案来解决。


alias 配置


报错Internal server error: Failed to resolve import "@/ok.ts" from "src/main.ts". Does the file exist?
image.png


通常我们的项目都会在 alias 中将 src 目录配置为 @ 来便于引用,所以遇到这个报错我们需要再 vite.config.js 中将之前 webpack 的 alias 配置补充进来(同时 vite 中 css 等样式文件的 alias 不需要加 ~ 前缀,所以也需要配置下 ~@):


import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'~@': path.resolve(__dirname, './src'),
// 其他的 alias 配置...
}
},
});
复制代码

css 全局变量


报错Internal server error: [less] variable @primaryColor is undefined
image.png


是因为项目在 less 文件中定义了变量,并在 webpack 的配置中通过 style-resources-loader 将其设置为了全局变量。我们可以在 vite.config.js 中添加如下配置引入文件将其设置为全局变量:


// vite.coonfig.js

export default defineConfig({
css: {
preprocessorOptions: {
less: {
additionalData: `@import "src/styles/var.less";`
},
},
},
});
复制代码

环境变量


报错ReferenceError: VUE_APP_HOST is not defined
image.png


这是因为项目中在 .env.local 文件中设置了以 VUE_APP_XXX 开头的环境变量,我们通过可以通过在 vite.config.js 的 define 中定义为全局变量:


// vite.config.js
export default defineConfig({
define: {
'process.env': {
NODE_ENV: import.meta.env,
APP_NAME: '我的项目名称',
},
+ VUE_APP_HOST: '"pinyin-pro.com"', // 这里需要注意定义为一个字符串
},
})
复制代码

process 未定义


报错ReferenceError: process is not defined
image.png


这是因为 webpack 启动时会根据 node 环境将代码中的 process 变量会将值给替换,而 vite 未替换该变量,所以在浏览器环境下会报错。


我们可以通过在 vite.config.js 中将 process.env 定义成一个全局变量,将相应的属性给配置好:


// vite.config.js
export default defineConfig({
define: {
'process.env': {
NODE_ENV: import.meta.env,
APP_NAME: '我的项目名称',
},
},
})
复制代码

使用 JSX


报错Uncaught ReferenceError: React is not defined
image.png


这是因为 react16 版本之后,babel 默认会将 .jsx/.tsx 语法转换为 react 函数,而我们需要以 vue 组件的方式来解析 .jsx/.tsx 文件,需要通过新的插件来解决:



  • 安装 @vitejs/plugin-vue-jsx 插件:
    yarn add @vitejs/plugin-vue-jsx -D
    复制代码


  • vite.config.js 文件中引入插件:
    // others
    import vueJsx from '@vitejs/plugin-vue-jsx';

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [
    vue(),
    vueJsx(),
    // others...
    ],
    });
    复制代码



CommonJS 不识别


报错ReferenceError: require is not defined
image.png


这是因为项目中通过 require() 引入了图片,webpack 支持 commonjs 语法,而 vite 开发环境是 esmodule 不支持 require。可以通过 @originjs/vite-plugin-commonjs 插件,它能解析 require 进行语法转换以支持同样效果:



  • 安装 @originjs/vite-plugin-commonjs 插件:
    yarn add @originjs/vite-plugin-commonjs -D
    复制代码


  • vite.config.js 中引入插件:
    import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

    export default defineConfig({
    plugins: [
    viteCommonjs()
    ]
    })
    复制代码



多模块导入


报错Uncaught ReferenceError: require is not defined
image.png


这个报错注意比前面的 ReferenceError: require is not defined 多了一个 Uncaught,是因为 @originjs/vite-plugin-commonjs 并不是对所有的 require 进行了转换,我们项目中还通过 webpack 提供的 require.context 进行了多模块导入。要解决这个问题可以通过 @originjs/vite-plugin-require-context 插件实现:



  • 安装 @originjs/vite-plugin-require-context 插件:
    yarn add @originjs/vite-plugin-require-context -D
    复制代码


  • vite.config.js 中引入插件:
    import ViteRequireContext from '@originjs/vite-plugin-require-context'

    export default defineConfig({
    plugins: [
    ViteRequireContext()
    ]
    })
    复制代码



其他 webpack 配置


其他的一些 webpack 配置例如 devServer 以及引用的一些 loader 和 plugin,只需要参考 vite 文档一一修改就行,由于各个团队的项目配置不同,我在这里就不展开了。需要注意的是,因为是开发环境下使用 vite,只需要适配开发环境的 webpack 配置就行,打包优化等不需要处理。


潜在隐患


上述方案中,我们通过不修改源代码 + 打包依然使用 webpack,保证了现有项目线上的稳定性:但还有一个潜在隐患:随着项目后期的迭代,因为开发环境是 vite,打包是 webpack,可能因为两种打包工具的不同导致开发和打包产物表现不同的缺陷。例如一旦你开发环境使用了 import.meta.xxx,打包后立马就会报错。


写在最后


我们当时采用此方案是因为 vite 刚发布没太久,用于正式环境有不少坑,而现在 vite 已经成为一款比较成熟的打包工具了,如果要迁移的话还是建议开发和打包都采用 vite,这种方面可以作为 webpack 迁移 vite 的短期过渡方案使用。(我们的项目现在打包也迁移到了 vite 了)


另外我们要明确,作为公司项目稳定性是第一位的,技术方案的变更需要明确能给项目带来收益。例如 webpack 迁移的 vite,是明确能够大幅优化开发环境的等待时间成本,而非看到别人都在用随大流而用。如果已知项目后期发展规模不会太大,当前项目启动时间也不长,就没有迁移的必要了。


上述迁移过程中遇到的坑只是针对我们的项目,没能包含全部的迁移坑点,大家有其他的遇到问题欢迎分享一起讨论。


最后推荐一个工具,可以将项目一键 webpack 迁移到 vite: webpack-to-vite


作者:zhoulx
链接:https://juejin.cn/post/7197222701220053047
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

10000+条数据的内容滚动功能如何实现?

web
遇到脑子有问题的产品经理该怎么办?如果有这么一个需求要你在一个可视区范围内不间断循环滚动几千上万条数据你会怎么去实现? 且不说提这个需求的人是不是脑子有问题,这个需求能不能实现?肯定是可以的,把数据请求回来渲染到页面写个滚动样式就好了。抛开这样一次性请求上万...
继续阅读 »

遇到脑子有问题的产品经理该怎么办?如果有这么一个需求要你在一个可视区范围内不间断循环滚动几千上万条数据你会怎么去实现?


录制_2022_11_25_20_43_47_705.gif


且不说提这个需求的人是不是脑子有问题,这个需求能不能实现?肯定是可以的,把数据请求回来渲染到页面写个滚动样式就好了。抛开这样一次性请求上万条数据合不合理不讲,一万条数据渲染到页面上估计都要卡死了吧。那有没有更好的方法呢? 当然有


分析一波思路


image.png
我们分批次请求数据,比如可视化区域展示的是20条数据,那我们就一次只请求30条,然后把请求回来的数据保存起来,定义一个滚动的数组,把第一次请求的30条数据赋值给它。后面每当有一条数据滚出可视区域我们就把它删掉,然后往尾部新增一条,让滚动数组始终保持30条的数据,这样渲染在页面上的数据始终只有30条而不是一万条。文字描述太生硬我们上代码


首先定义两个数组,一个滚动区域的数组scrollList,一个总数据的数组totalList,模拟一个异步请求的方法和获取数据的方法。


<script lang="ts" setup>
import { nextTick, ref } from "vue";
type cellType = {
id: number,
title: string,
}
interface faceRequest {
data: cellType,
total: number
}
// 总数据的数组
const totalList = ref<Array<cellType>>([]);
// 滚动的数组
const scrollList = ref<Array<cellType>>([]);
// 数据是否全部加载完毕
let loading: Boolean = false
// 模拟异步请求
const request = () => {
return new Promise<faceRequest>((resolve: any, reject: any) => {
let data: Array<cellType> = []
// 每次返回30条数据
for (let i = 0; i < 30; i++) {
data.push({
id: totalList.value.length + i,
title: 'cell---' + (totalList.value.length + i)
});
}
let total = 10000// 数据的总数
resolve({ data, total })
})
}
const getData = () => {
request().then(res => {
totalList.value = totalList.value.concat(res.data)

// 默认获取第一次请求回来的数据
if (totalList.value.length <= 30) {
scrollList.value = scrollList.value.concat(res.data)
}
// 当前请求的数量小于总数则继续请求
if (totalList.value.length < res.total) {
getData()
} else {
loading = true
}
})
}
getData()
</script>
复制代码

上面写好了数据的获取处理,接下来写一下页面


<template>
<div class="div">
<div :style="styleObj" @mouseover="onMouseover" @mouseout="onMouseout" ref="divv">
<div v-for="item in scrollList" :key="item.id" @click="onClick(item)">
<div class="cell">{{ item.title }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// 滚动样式
const styleObj = ref({
transform: "translate(0px, 0px)",
});
</script>
<style scoped>
.div {
width: 500px;
height: 500px;
background-color: aquamarine;
overflow: hidden;
}

.cell {
height: 30px;
}
</style>
复制代码

现在页面跟数据的前提条件都写好,下面就是数据逻辑的处理了,也就是这篇文章的重点



  1. 获取页面上单条数据的总体高度

  2. 设置定时器使页面不停的滚动

  3. 当一条数据滚动出视图范围时调用处理数据的方法并且重置滚动高度为0


const divv = ref();
// 当前滚动高度
const ScrollHeight = ref<number>(0);
// 储存定时器
const setInt = ref();
// 内容滚动
const roll = () => {
nextTick(() => {
let offsetHeight = divv.value.childNodes[1].offsetHeight
setInt.value = setInterval(() => {
if (ScrollHeight.value == offsetHeight) {
onDel();
ScrollHeight.value = 0;
}
ScrollHeight.value++;
styleObj.value.transform = `translate(0px, -${ScrollHeight.value}px)`;
}, 10);
})
};
onMounted(() => {
roll()
})
复制代码

处理数据的方法



  1. 保存需要被删除的数据

  2. 删除超出视窗的数据

  3. 获取总数组的数据添加到滚动数组的最后一位

  4. 将被删除的数组数据添加到总数组最后面,

  5. 当滚动到最后一条数据时重置下标为0,使得数据首位相连不断循环


let index = 29;// 每次请求的数量-1,例如每次请求30条数据则为29
const onDel = () => {
index++;
if (loading) {
// 当滚动到最后一条数据时重置下标为0
if (index == totalList.value.length) {
index = 0;
}
scrollList.value.shift();
scrollList.value.push(totalList.value[index]);
} else {
if (index == totalList.value.length) {
index = 0;
}
// 保存需要被删除的数据
let value = scrollList.value[0]
// 删除超出视窗的数据
scrollList.value.shift();
// 获取总数组的数据添加到滚动数组的最后一位
scrollList.value.push(totalList.value[index]);
// 将被删除的数组数据添加到总数组最后面
totalList.value.push(value)
}
};
复制代码

到这里代码就写好了,接下来让我们看看效果怎么样


image.png


总结


在我们开发的过程中会遇到各种各样天马行空的需求,尤其会遇到很多不合理的需求,这时候我们就要三思而后行,


想清楚能不能不做?


能不能下次再做?


能不能让同事去做?


作者:清慕
链接:https://juejin.cn/post/7169940462357184525
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

CSS动画篇之404动画

web
当前页面无法访问,可能没有权限或已删除。 作为一个从事互联网行业的你是不是见过各种各种的404页面,今天刚好发现一个比较有趣的404页面,如上图所示,是不是感觉挺炫酷呢,本文将和大家分享一下实现原理。 前言 看到上面的404你的第一感觉会是这么做呢? 来,UI...
继续阅读 »

404
当前页面无法访问,可能没有权限或已删除。 作为一个从事互联网行业的你是不是见过各种各种的404页面,今天刚好发现一个比较有趣的404页面,如上图所示,是不是感觉挺炫酷呢,本文将和大家分享一下实现原理。


前言


看到上面的404你的第一感觉会是这么做呢?


来,UI同学给我上GIF。
当然这种方式对于前端同学来说肯定是最简单的实现方式,单纯的加载一张图片即可。


但是对于一个有追求的前端,绝对不会答应这么干,加载一张GIF图片的成本太高了,网络差的情况下会导致白屏时间过长,所以我们尽可能的用代码实现,减少这种不必要的网络请求。


实现


当你仔细看这个动画的时候可以发现其实主体只有一个标签,内容就是404,另外的几个动画都是基于这个主体实现,所以我们先写好这个最简单的html代码。


<h1 data-t="404">404</h1>
复制代码

细心的同学应该看到了我们自定义了一个熟悉data-t,这个我们后续在css中会用到,接下来实现主体的动画效果,主要的动画效果就是让主体抖动并增加模糊的效果,代码实现如下所示。


h1 {
text-align: center;
width: 100%;
font-size: 6rem;
animation: shake .6s ease-in-out infinite alternate;
}

@keyframes shake {
0% {
transform: translate(-1px)
}

10% {
transform: translate(2px, 1px)
}

30% {
transform: translate(-3px, 2px)
}

35% {
transform: translate(2px, -3px);
filter: blur(4px)
}

45% {
transform: translate(2px, 2px) skewY(-8deg) scaleX(.96);
filter: blur(0)
}

50% {
transform: translate(-3px, 1px)
}
}
复制代码

接下来增加主体动画后面子两个子动画内容,基于伪元素实现,伪元素的内容通过上面html中自定义data-t获取,主要还用了clip中的rect,具体css代码如下。


h1:before {
content: attr(data-t);
position: absolute;
left: 50%;
transform: translate(-50%,.34em);
height: .1em;
line-height: .5em;
width: 100%;
animation: scan .5s ease-in-out 275ms infinite alternate,glitch-anim .3s ease-in-out infinite alternate;
overflow: hidden;
opacity: .7;
}

@keyframes glitch-anim {
0% {
clip: rect(32px,9999px,28px,0)
}

10% {
clip: rect(13px,9999px,37px,0)
}

20% {
clip: rect(45px,9999px,33px,0)
}

30% {
clip: rect(31px,9999px,94px,0)
}

40% {
clip: rect(88px,9999px,98px,0)
}

50% {
clip: rect(9px,9999px,98px,0)
}

60% {
clip: rect(37px,9999px,17px,0)
}

70% {
clip: rect(77px,9999px,34px,0)
}

80% {
clip: rect(55px,9999px,49px,0)
}

90% {
clip: rect(10px,9999px,2px,0)
}

to {
clip: rect(35px,9999px,53px,0)
}
}

@keyframes scan {
0%,20%,to {
height: 0;
transform: translate(-50%,.44em)
}

10%,15% {
height: 1em;
line-height: .2em;
transform: translate(-55%,.09em)
}
}
复制代码

伪元素after的动画与before中的一致,只是部分参数改动,如下所示。


h1:after {
content: attr(data-t);
position: absolute;
top: -8px;
left: 50%;
transform: translate(-50%,.34em);
height: .5em;
line-height: .1em;
width: 100%;
animation: scan 665ms ease-in-out .59s infinite alternate,glitch-anim .3s ease-in-out infinite alternate;
overflow: hidden;
opacity: .8
}
复制代码

总结


到此为止我们的功能就实现完成啦,看完代码是不是感觉并没有很复杂,又为我们的页面性能提升了大大的一步。


完整的代码可以访问codepen查看 👉 codepen-404


作者:南城FE
链接:https://juejin.cn/post/7091848998830473230
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Compose跨平台又来了,这次能开发iOS了

/   今日科技快讯   /近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。目前,文心一言在做上线前的冲刺。百度方面表示,...
继续阅读 »
/   今日科技快讯   /

近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。

目前,文心一言在做上线前的冲刺。百度方面表示,ChatGPT相关技术,百度都有。百度在人工智能四层架构中,有全栈布局。包括底层的芯片、深度学习框架、大模型以及最上层的搜索等应用。文心一言,位于模型层。

/   作者简介   /

本篇文章转自黄林晴的博客,文章主要分享了如何使用Compose来进行IOS开发,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7195770699524751421

/   前言   /

在之前,我们已经体验了Compose for Desktop与Compose for Web,目前Compose for IOS已经有尚未开放的实验性API,乐观估计今年年底将会发布 Compose for IOS。同时Kotlin也表示将在2023年发布KMM的稳定版本。



届时Compose-jb + KMM将实现Kotlin全平台。



/   搭建项目   /

创建项目

因为目前Compose for iOS阶段还在试验阶段,所以我们无法使用Android Studio或者IDEA直接创建Compose支持IOS的项目,这里我们采用之前的方法,先使用Android Studio创建一个KMM项目,如果你不知道如何创建一个KMM项目,可以参照之前的这篇文章KMM的初次尝试~,项目目录结构如下所示。



创建好KMM项目后我们需要添加Compose跨平台的相关配置。

添加配置

首先在settings.gradle文件中声明compose插件,代码如下所示:

pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }

    plugins {
        val composeVersion = extra["compose.version"as String
        id("org.jetbrains.compose").version(composeVersion)
    }
}

这里compose.version的版本号是声明在gradle.properties中的,代码如下所示:

compose.version=1.3.0

然后我们在shared模块中的build文件中引用插件:

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
    id("org.jetbrains.compose")
}

并为commonMain添加compose依赖,代码如下所示:

val commonMain by getting {
    dependencies {
        implementation(compose.ui)
        implementation(compose.foundation)
        implementation(compose.material)
        implementation(compose.runtime)
    }
}

sync之后,你会发现一个错误警告:uikit还处于试验阶段并且有许多bug....



uikit就是compose-jb暴露的UIKit对象。为了能够使用,我们需要在gradle.properties文件中添加如下配置:

org.jetbrains.compose.experimental.uikit.enabled=true

添加好配置之后,我们先来运行下iOS项目,确保添加的配置是无误的。果然,不运行不知道,一运行吓一跳。



这个问题困扰了我两三天,实在是无从下手,毕竟现在相关的资料很少,经过N次的搜索,最终解决的方案很简单:Kotlin版本升级至1.8.0就可以了。

kotlin("android").version("1.8.0").apply(false)

再次运行项目,结果如下图所示。



不过这是KMM的iOS项目,接下来我们看如何使用Compose编写iOS页面。

/   开始iOS之旅   /

我们替换掉iOSApp.swift中的原有代码,替换后的代码如下所示:

import UIKit
import shared

@UIApplicationMain
class AppDelegateUIResponderUIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController
        window?.makeKeyAndVisible()
        return true
    }
}

上面的代码看不懂没关系,我们只来看获取mainViewController的这一行:

let mainViewController = Main_iosKt.MainViewController()

Main_iosKt.MainViewController是通过新建在shared模块iOSMain目录下的main.ios.kt文件获取的,代码如下所示:

fun MainViewController(): UIViewController = Application("Login") { //调用一个Compose方法 }

接下来所有的事情就都可以交给Compose了。

图片实现一个登录页面

因为页面这部分是公用的,所以我们在shared模块下的commonMain文件夹下新建Login.kt文件,编写一个简单的登录页面,代码如下所示:

@Composable
internal fun login() {
    var userName by remember {
        mutableStateOf("")
    }
    var password by remember {
        mutableStateOf("")
    }
    Surface(modifier = Modifier.padding(30.dp)) {
        Column {
            TextField(userName, onValueChange = {
                userName = it
            }, placeholder = { Text("请输入用户名") })
            TextField(password, onValueChange = {
                password = it
            }, placeholder = { Text("请输入密码") })
            Button(onClick = {
                //登录
            }) {
                Text("登录")
            }
        }
    }
}

上述代码声明了一个用户名输入框、密码输入框和一个登录按钮,就是简单的Compose代码。然后需要在main.ios.kt中调用这个login方法:

fun MainViewController(): UIViewController =
    Application("Login") {
        login()
    }

运行iOS程序,效果如下图所示:



嗯~,Compose 在iOS上UI几乎可以做到100%复用,还有不学习Compose的理由吗?

实现一个双端网络请求功能

在之前的第1弹和第2弹中,我们分别实现了在Desktop、和Web端的网络请求功能,现在我们对之前的功能在iOS上再次实现。

添加网络请求配置

首先在shared模块下的build文件中添加网络请求相关的配置,这里网络请求我们使用Ktor,具体的可参照之前的文章:KMM的初次尝试~

配置代码如下所示:

val commonMain by getting {
    dependencies {
        ...
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
        implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        implementation("io.ktor:ktor-client-core:$ktorVersion")
        implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
        implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    }
}
val iosMain by getting {
    dependencies {
        implementation("io.ktor:ktor-client-darwin:$ktorVersion")
    }
}

val androidMain by getting {
    dependencies {
        implementation("io.ktor:ktor-client-android:$ktorVersion")
    }
}

添加接口

这里我们仍然使用wandroid中的每日一问接口。DemoReqData与之前系列的实体类是一样的,这里就不重复展示了。接口地址如下:
https://wanandroid.com/wenda/list/1/json

创建接口地址类,代码如下所示:

object Api {
    val dataApi = "https://wanandroid.com/wenda/list/1/json"
}

创建HttpUtil类,用于创建HttpClient对象和获取数据的方法,代码如下所示。

class HttpUtil {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
    }

    /**
     * 获取数据
     */

    suspend fun getData(): DemoReqData {
        val rockets: DemoReqData =
            httpClient.get(Api.dataApi).body()
        return rockets
    }
}

这里的代码我们应该都是比较熟悉的,仅仅是换了一个网络请求框架而已。现在公共的业务逻辑已经处理好了,只需要页面端调用方法然后解析数据并展示即可。

编写UI层

由于Android、iOS、Desktop三端的UI都是完全复用的,所以我们将之前实现的UI搬过来即可。代码如下所示:

Column() {
    val scope = rememberCoroutineScope()
    var demoReqData by remember { mutableStateOf(DemoReqData()) }
    Button(onClick = {
        scope.launch {
            try {
                demoReqData = HttpUtil().getData()
            } catch (e: Exception) {
            }
        }
    }) {
        Text(text = "请求数据")
    }

    LazyColumn {
        repeat(demoReqData.data?.datas?.size ?: 0) {
            item {
                Message(demoReqData.data?.datas?.get(it))
            }
        }
    }
}

获取数据后,通过Message方法将数据展示出来。这里只将作者与标题内容显示出来,代码如下所示:

@Composable
fun Message(dataDemoReqData.DataBean.DatasBean?) {
    Card(
        modifier = Modifier
            .background(Color.White)
            .padding(10.dp)
            .fillMaxWidth(), elevation = 10.dp
    ) {
        Column(modifier = Modifier.padding(10.dp)) {
            Text(
                text = "作者:${data?.author}"
            )
            Text(text = "${data?.title}")
        }
    }
}

分别运行iOS、Android程序,点击请求数据按钮,结果如下图:



这样我们就用一套代码,实现了在双端的网络请求功能。

/   一个尴尬的问题   /

我一直认为存在一个比较尴尬的问题,那就是像上面实现一个完整的双端网络请求功能需要用到KMM + Compose-jb,但是KMM与Compose-jb并不是一个东西,但是用的时候呢基本上都是一起用。Compose-jb很久之前已经发了稳定版本只是Compose-iOS目前还没有开放出来,而KMM当前还处于试验阶段,不过在2023年Kotlin的RoadMap中,Kotlin已经表示将会在23年中发布第一个稳定版本的KMM。而Compose for iOS何时发布,我想也是指日可待的事情。

所以,这个系列我觉得改名为:Kotlin跨平台系列更适合一些,要不然以后就会存在KMM跨平台第n弹,Compse跨平台第n弹....

因此,从第四弹开始,此系列将更名为:Kotin跨平台第N弹:~

/   写在最后   /

从自身体验来讲,我觉得KMM+Compose-jb对Android开发者来说是非常友好的,不需要像Flutter那样还需要额外学习Dart语言。所以,你觉得距离Kotlin一统“江山”的日子还会远吗?

该文章转载自:https://mp.weixin.qq.com/s/LfD6AD-gDFdEYQS1X96CGw
收起阅读 »

Kotlin | 这些隐藏的内存陷阱,你应该熟记于心

引言 Kotlin 是一个非常 yes 的语言,从 null安全 ,支持 方法扩展 与 属性扩展,到 内联方法、内联类 等,使用Kotlin变得越来越简单舒服。但编程从来不是一件简单的工作,所有简洁都是建立在复杂的底层实现上。那些看似简单的kt代码,内部往往隐...
继续阅读 »

引言


Kotlin 是一个非常 yes 的语言,从 null安全 ,支持 方法扩展属性扩展,到 内联方法内联类 等,使用Kotlin变得越来越简单舒服。但编程从来不是一件简单的工作,所有简洁都是建立在复杂的底层实现上。那些看似简单的kt代码,内部往往隐藏着不容忽视的内存开销。


介于此,本篇将根据个人开发经验,聊一聊 Kotlin 中那些隐藏的内存陷阱,也希望每一个同学都能在 性能优雅 之间找到合适的平衡。



本篇定位简单🔖,主要通过示例+相应字节码分析的方式,对日常开发非常有帮助。





密封类的小细节


密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。摘自Kotlin中文文档



关于它用法,我们具体不再做赘述。



密封类虽然非常实用,经常能成为我们多type的绝佳搭配,但其中却藏着一些使用的小细节,比如 构造函数传值所导致的损耗问题。


错误示例


image.png
如题, 我们有一个公用的属性 sum ,为了便于复用,我们将其抽离到 Fruit 类构造函数中,让子类便于初始化时传入,而不用重复显式声明。


上述代码看着似乎没什么问题?按照传统的操作习惯,我们也很容易写出这种代码。


如果我们此时来看一下字节码:


image-20221022080336140


不难发现,无论是子类Apple还是父类Fruit,他们都生成了 getSum()setSum() 方法 与 sum 字段,而且,父类的 sum 完全处于浪费阶段,我们根本没法用到。😵‍💫


显然这并不是我们愿意看到的,我们接下来对其进行改造一下。


改造实践


我们对上述示例进行稍微改造,如下所示:


image.png
如题,我们将sum变量定义为了一个抽象变量,从而让子类自行实现。对比字节码可以发现,相比最开始的示例,我们的父类 Fruit 中减少了一个 sum 变量的损耗。




那有没有方法能不能把 getsum()setSum() 也一起移除呢?🙅‍♂️


答案是可以,我们利用 接口 改造即可,如下所示:


image-20221018100240436


如上所示,我们增加了一个名为 IFruit 的接口,并让 密封父类 实现了这个接口,子类默认在构造函数中实现该属性即可。


观察字节码可发现,我们的父类一干二净,无论是从包大小还是性能,我们都避免了没必要的损耗。


内联很好,但别太长


inline ,翻译过来为 内联 ,在 Kotlin 中,一般建议用于 高阶函数 中,目的是用来弥补其运行时的 额外开销


其原理也比较简单,在调用时将我们的代码移动到调用处使用,从而降低方法调用时的 栈帧 层级。



栈帧: 指的是虚拟机在进行方法调用和方法执行时的数据结构,每一个栈帧里都包含了相应的数据,比如 局部参数,操作数栈等等。


Jvm在执行方法时,每执行一个方法会产生一个栈帧,随后将其保存到我们当前线程所对应的栈里,方法执行完毕时再将此方法出栈,


所以内联后就相当于省了一个栈帧调用。



如果上述描述中,你只记住了后半句,降低栈帧 ,那么此时你可能已经陷入了一个使用陷阱?


错误示例


如下截图中所示,我们随便创建了一个方法,并增加了 inline 关键字:


image-20221012230619100


观察截图会发现,此时IDE已经给出了提示,它建议你移除 inline , Why? 为什么呢?🥲



不是说内联可以提高性能吗,那么不应该任何方法都应该加 inline 提高性能吗?(就是这么倔强🤌🏼)



上面我们提到了,内联是会将代码移动到调用处,降低 一层栈帧,但这个性能提升真的大吗?


再仔细想想,移动到调用处,移动到调用处。这是什么概念呢?



假设我们某个方法里代码只有两行(我想不会有人会某个方法只有一行吧🥲),这个方法又被好几处调用,内联是提高了调用性能,毕竟节省了一次栈帧,再加上方法行数少(暂时抛弃虚拟机优化这个底层条件)。


但如果方法里代码有几十行?每次调用都会把代码内联过来,那调用处岂不💥,带来的包大小影响某种程度上要比内联成本更高😵‍💫!



如下图所示,我们对上述示例做一个论证:


image-20221012232807919



Jvm: 我谢谢你。



推荐示例


我们在文章最开始提到了,Kotlin inline ,一般建议用于 高阶函数(lambda) 中。为什么呢?


如下示例:


image-20221013094634526


转成字节码后,可以发现,tryKtx() 被创建为了一个匿名内部类 (Simple$test|1) 。每次调用时,相当于需要创建匿名类的实例对象,从而导致二次调用的性能损耗。


那如果我们给其增加 inline 呢?🤖,反编译后相应的 java代码 如下:


image-20221013100057206


具体对比图如上所示,不难发现,我们的调用处已经被替换为原方法,相应的 lambda 也被消除了,从而显著减少了性能损耗。


Tips


如果查看官方库相应的代码,如下所示,比如 with :


image.png


不难发现,inline 的大多数场景仅且在 高阶函数 并且 方法行数较短 时适用。因为对于普通方法,jvm本身对其就会进行优化,所以 inline 在普通方法上的的意义几乎聊胜于无。


总结如下:



  • 因为内联函数会将方法函数移动到调用处,会增加调用处的代码量,所以对于较长的方法应该避免使用

  • 内联函数应该用于使用了 高阶函数(lambda) 的方法,而不是普通方法。



伴生对象,也许真的不需要


Kotlin 中,我们不能像 Java 一样,随便定义一个静态方法或者静态属性。此时 companion object(伴生对象)就会派上用场。


我们常常会用于定义一个 key 或者 TAG ,类似于我们在 Java 中定义一个静态的 Key。其使用起来也很简单,如下所示:


class Book {
companion object {
val SUM_MAX: Int = 13
}
}

这是一段普通的代码,我们在 Book 类中增加了一个伴生对象,其中有一个静态的字段 SUM_MAX。


上述代码看着似乎没什么问题,但如果我们将其转为字节码后再看一看:


image-20221024091359601


不难发现,仅仅只是想增加一个 静态变量 ,结果凭空增加了一个 静态对象 以及多增加了 get() 方法,这个成本可能远超出一个 静态参数 的价值。




const


抛开前者不谈(静态对象),那么我们有没有什么方法能让编译器少生成一个 get() 方法呢(非private)?


注意观察IDE提示,IDE会建议我们增加一个 const 的参数,如下所示:


companion object {
const val SUM_MAX: Int = 13
}

增加了 const 后,相应的 get() 方法也会消失掉,从而节省了一个 get() 方法。



const,在 Kotlin 中,用于修饰编译时已知的 val(只读,类似final) 标注的属性。



  • 只能用于顶层的class中,比如 object class 或者 companion object

  • 只能用于基本类型;

  • 不会生成get()方法。





JvmField


如果我们 某个字段不是 val 标注呢,其是 var (可变)修饰的呢,并且这个字段要对外暴漏(非private)。



此时不难猜测,相应的字节码后肯定会同时生成 set与get 方法。



此时就可以使用 @JvmField 来进行修饰。


如下所示:


class Book {
companion object {
@JvmField
var sum: Int = 0
}
}

相应的字节码如下:
image-20221024142110409


Tips


让我们再回到伴生对象本身,我们真的一定需要它吗?


对于和业务强关联的 key 或者 TAG ,可以选择使用伴生对象,并为其增加 const val,此时语义上的清晰比内存上的损耗更加重要,特别在复杂的业务背景下。


但如果仅用于保存一些key,那么完全可以使用 object Class 替代,如下所示,将其回归到一个类中:


object Keys {
const val DEFAULT_SUM = 10
const val DEFAULT_MIN = 1
const val LOGIN_KEY = 99
}



2022/12/6补充


使用 kotlin 文件形式去写。


这种写法属于以增加静态类的方式避免伴生对象的内存损耗,如果你的场景是单独的增加一个tag,那么这种写法比较推荐。



对于sdk的开发者,同时建议增加 @file:JvmName(“ 文件名”) ,从而禁止生成的 xxxkt类 在 java 语境下被调用到 (欺负java不识别空格🤪)。



@file:JvmName(" Testxx")

private const val TAG = "KEY_TEST_TAG"

class TestKt {
   private fun test() {
       println(TAG)
  }
}


Apply!=构造者模式


apply 作为开发中的常客,为我们带来了不少便利。其内部实现也非常简单,将我们的对象以函数的形式返回,this 作为接收者。从而以一种优雅的方式实现对对象方法、属性的调用。


但经常会看到有不少同学在构造者模式中写出以下代码,使用 apply 直接作为返回值,这种方式固然看着优雅,性能也几乎没有差别。但这种场景而言,如果我们注意到其字节码,会发现其并不是最佳之选。


示例


image-20221022082110693


如题,我们存在一个示例Builder,并在其中添加了两个方法,即 addTitle(),与 addSecondTitle() 。后者以 apply 作为返回值,代码可读性非常好,相比前者,在 kotlin 中其显得非常优雅。


但如果我们去看一眼字节码呢?


image-20221022082523238


如上所示,使用了 apply 后,我们的字节码中增加了多余步骤,相比不使用的,包大小会有一点影响,性能上几乎毫无差距。


Tips


apply 很好用,但需要区分场景。其可以改善我们在 kotlin 语义下的编程体验,但同时也不是任何场景都需要其。


如果你的方法中需要对某个对象操作多次,比如调用其方法或者属性,那么此时可以使用 apply ,反之,如果次数过少,其实你并不需要 apply 的优雅。


警惕,lazy 的使用方式


lazy,中文译名为延迟初始化,顾名思义,用于延迟初始化一些信息。


作用也相对直接,如果我们有某个对象或字段,我们可能只想使用时再初始化,此时就可以先声明,等到使用时再去初始化,并且这个初始化过程默认也是线程安全(不特定使用NONE)。这样的好处就是性能优势,我们不必应用或者页面加载时就初始化一切,相比过往的 var xx = null ,这种方式一定程度上也更加便捷。


相应的,lazy一共有三种模式,即:



  • SYNCHRONIZED(同步锁,默认实现)

  • PUBLICATION(CAS)

  • NONE(不作处理)


lazy 虽然使用简单,但在 Android 的开发背景下,lazy 经常容易使用不当🤦🏻‍♂️,也因此常常会出现为了[便利] 而造成的性能隐患。


示例如下:


image.png


如上所示,我们延迟初始化了一个点击事件,方便在 onCreate() 中进行设置 点击事件 以及后续复用


上述示例虽然看着似乎没什么问题。但放在这样的场景下,这个 mClickListener 本身的意义也许并不大。为什么这样说?




  1. 上述使用了 默认的lazy ,即同步锁,而Android默认线程为 UI线程 ,当前操作方法又是 onCreate() ,即当前本身就是线程安全。此时依然使用 lazy(sys) ,即浪费了一定初始化性能。

  2. MainActivity初始化时,会先在 构造函数 中初始化 lazy 对象,即 SYNCHRONIZED 对应的 SynchronizedLazyImpl。也就是说,我们一开始就已经多生成了一个对象。然后仅仅是为了一个点击事件,内部又会进行包装一次



相似的场景有很多,如果你的lazy是用于 Android生命周期组件 ,再加上本身会在 onCreate() 等中进行调用,那么很可能完全没有必要延迟初始化。


关于 arrayOf() 的使用细节


对于 arrayOf ,我们一般经常用于初始化一个数组,但其也隐藏着一些使用细节。


通常来说,对于基本类型的数组,建议使用默认已提供的函数比如,intArrayOf() 等等,从而便于提升性能。


至于原因,我们下面来分析,如下所示:


fun test() {
arrayOf(1, 2, 3)
}

fun testNoInteger() {
intArrayOf(1, 2, 3)
}

我们提供了两个方法,前者是默认方法,后者是带优化的方法,具体字节码如下:


image-20221022095310870


如题,不难发现,前者使用的是 java 中的 包装类型 ,使用时还需要经历 拆箱装箱 ,而后者是非包装类型,从而免除了这一操作,从而节省性能。



什么是装箱与拆箱?


背景:Java 中,万物皆对象,而八大基本类型不是对象,所以 Java 为每种基本类型都提供了相应的包装类型。


装箱就是指将基本类型转为包装类型,拆箱则是将包装类型转为基本类型。



总结


本篇中,我们以日常开发的视角,去探寻了 Kotlin 中那些 [隐藏] 的内存陷阱。


仔细回想,上述的不恰当用法都是建立在 [不熟练] 的背景下。Kotlin 本身的各种便利没有任何问题,其使得我们的 代码可读性开发舒适度 增强了太多。但如果同时,我们还能注意到其背后的实现,也是不是就能在 性能与优雅 之间找到了一种平衡。


所谓左眼 kt ,右眼 java,正是如此。作为一个 Kotlin 使用者,这也是我们所不断追寻的。



善用字节码分析,你的技艺也将更上一筹。



参阅



关于我


我是 Petterp ,一个三流 Kotlin 使用者,如果本文对你有所帮助,欢迎点赞评论收藏,你的支持是我持续创作的最大鼓励!


作者:Petterp
链接:https://juejin.cn/post/7157905051531345956
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

新项目为什么决定用 JDK 17了

最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。 JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?” 不光是我呀,连 Spring Boot 都开始要拥护 ...
继续阅读 »

最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。


JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?”


不光是我呀,连 Spring Boot 都开始要拥护 JDK 17了,下面这一段是 Spring Boot 3.0 的更新日志。



Spring Boot 3.0 requires Java 17 as a minimum version. If you are currently using Java 8 or Java 11, you'll need to upgrade your JDK before you can develop Spring Boot 3.0 applications.



Spring Boot 3.0 需要 JDK 的最低版本就是 JDK 17,如果你想用 Spring Boot 开发应用,你需要将正在使用的 Java 8 或 Java 11升级到 Java 17。


选用 Java 17,概括起来主要有下面几个主要原因:


1、JDK 17 是 LTS (长期支持版),可以免费商用到 2029 年。而且将前面几个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本;


2、JDK 17 性能提升不少,比如重写了底层 NIO,至少提升 10% 起步;


3、大多数第三方框架和库都已经支持,不会有什么大坑;


4、准备好了,来吧。


拿几个比较好玩儿的特性来说一下 JDK 17 对比 JDK 8 的改进。


密封类


密封类应用在接口或类上,对接口或类进行继承或实现的约束,约束哪些类型可以继承、实现。例如我们的项目中有个基础服务包,里面有一个父类,但是介于安全性考虑,值允许项目中的某些微服务模块继承使用,就可以用密封类了。


没有密封类之前呢,可以用 final关键字约束,但是这样一来,被修饰的类就变成完全封闭的状态了,所有类都没办法继承。


密封类用关键字 sealed修饰,并且在声明末尾用 permits表示要开放给哪些类型。


下面声明了一个叫做 SealedPlayer的密封类,然后用关键字 permits将集成权限开放给了 MarryPlayer类。


public sealed class SealedPlayer permits MarryPlayer {
public void play() {
System.out.println("玩儿吧");
}
}

之后 MarryPlayer 就可以继承 SealedPlayer了。


public non-sealed class MarryPlayer extends SealedPlayer{
@Override
public void play() {
System.out.println("不想玩儿了");
}
}

继承类也要加上密封限制。比如这个例子中是用的 non-sealed,表示不限制,任何类都可以继承,还可以是 sealed,或者 final


如果不是 permits 允许的类型,则没办法继承,比如下面这个,编译不过去,会给出提示 "java: 类不得扩展密封类:org.jdk17.SealedPlayer(因为它未列在其 'permits' 子句中)"


public non-sealed class TomPlayer extends SealedPlayer {

@Override
public void play() {

}
}

空指针异常


String s = null;
String s1 = s.toLowerCase();

JDK1.8 的版本下运行:


Exception in thread "main" java.lang.NullPointerException
at org.jdk8.App.main(App.java:10)

JDK17的版本(确切的说是14及以上版本)


Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "s" is null
at org.jdk17.App.main(App.java:14)

出现异常的具体方法和原因都一目了然。如果你的一行代码中有多个方法、多个变量,可以快速定位问题所在,如果是 JDK1.8,有些情况下真的不太容易看出来。


yield关键字


public static int calc(int a,String operation){
var result = switch (operation) {
case "+" -> {
yield a + a;
}
case "*" -> {
yield a * a;
}
default -> a;
};
return result;
}

换行文本块


如果你用过 Python,一定知道Python 可以用 'hello world'"hello world"''' hello world '''""" hello world """ 四种方式表示一个字符串,其中后两种是可以直接支持换行的。


在 JDK 1.8 中,如果想声明一个字符串,如果字符串是带有格式的,比如回车、单引号、双引号,就只能用转义符号,例如下面这样的 JSON 字符串。


String json = "{\n" +
" \"name\": \"古时的风筝\",\n" +
" \"age\": 18\n" +
"}";

从 JDK 13开始,也像 Python 那样,支持三引号字符串了,所以再有上面的 JSON 字符串的时候,就可以直接这样声明了。


String json = """
{
"name": "古时的风筝",
"age": 18
}
""";

record记录类


类似于 Lombok 。


传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。


之前声明一个实体类。


public class User {
private String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

使用 Record类之后,就像下面这样。


public record User(String name) {

}

调用的时候像下面这样


RecordUser recordUser = new RecordUser("古时的风筝");
System.out.println(recordUser.name());
System.out.println(recordUser.toString());

输出结果



Record 类更像是一个实体类,直接将构造方法加在类上,并且自动给字段加上了 getter 和 setter。如果一直在用 Lombok 或者觉得还是显式的写上 getter 和 setter 更清晰的话,完全可以不用它。


G1 垃圾收集器


JDK8可以启用G1作为垃圾收集器,JDK9到 JDK 17,G1 垃圾收集器是默认的垃圾收集器,G1是兼顾老年代和年轻代的收集器,并且其内存模型和其他垃圾收集器是不一样的。


G1垃圾收集器在大多数场景下,其性能都好于之前的垃圾收集器,比如CMS。


ZGC


从 JDk 15 开始正式启用 ZGC,并且在 JDK 16后对 ZGC 进行了增强,控制 stop the world 时间不超过10毫秒。但是默认的垃圾收集器仍然是 G1。


配置下面的参数来启用 ZGC 。


-XX:+UseZGC

可以用下面的方法查看当前所用的垃圾收集器


JDK 1.8 的方法


jmap -heap 8877

JDK 1.8以上的版本


jhsdb jmap --heap --pid 8877

例如下面的程序采用 ZGC 垃圾收集器。



其他一些小功能


1、支持 List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法实例化对象;


2、Stream API 有一些改进,比如 .collect(Collectors.toList())可以直接写成 .toList()了,还增加了 Collectors.teeing(),这个挺好玩,有兴趣可以看一下;


3、HttpClient重写了,支持 HTTP2.0,不用再因为嫌弃 HttpClient 而使用第三方网络框架了,比如OKHTTP;


升级 JDK 和 IDEA


安装 JDK 17,这个其实不用说,只是推荐一个网站,这个网站可以下载各种系统、各种版本的 JDK 。地址是 adoptium.net/


还有,如果你想在 IDEA 上使用 JDK 17,可能要升级一下了,只有在 2021.02版本之后才支持 JDK 17。


作者:古时的风筝
链接:https://juejin.cn/post/7177550894316126269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我的个人微信也迅速接入了 ChatGPT

本文主要来聊聊如何快速使用个人微信接入 ChatGPT,欢迎 xdm 尝试起来,仅供学习参考,切莫用于做不正当的事情 关于 ChatGPT 我们每个人都可以简单的使用上,不需要你有很强的技术背景,不需要你有公众号,甚至不需要你自己接入,只要你有一个微信号,就可...
继续阅读 »

本文主要来聊聊如何快速使用个人微信接入 ChatGPT,欢迎 xdm 尝试起来,仅供学习参考,切莫用于做不正当的事情


关于 ChatGPT 我们每个人都可以简单的使用上,不需要你有很强的技术背景,不需要你有公众号,甚至不需要你自己接入,只要你有一个微信号,就可以享受到 ChatGPT 带给你的惊喜,那么我们开始吧


本文分别从如下几个方面来聊:



  • ChatGPT 是什么




  • 个人微信如何快速接入 ChatGPT




  • 关于 ChatGPT 的思考


ChatGPT 是什么



ChatGPT 实际上一个以对话的形式来回答各种问题的模型,他的名字叫做 ChatGPT ,简单理解,他就是一个聊天机器人


现实中,我们印象中的机器人总是那么死板和固执,但是 ChatGPT 却是一个非常人性化的对话模型,他可以和我们聊天,回答我们的各种问题,并且上下文他是可以做到关联的


甚至在未来你在使用各种应用软件的时候,与你对接的客服,或许就是这样的机器人,你可能完全感受不到他居然能够像人一样,甚至比人还聪明,比人还博学


个人微信如何快速接入 ChatGPT


个人微信一样能够轻松的接入 ChatGPT ,不需要你有公众号,只需要做对接的人满足如下资源即可:



  • 经过实名认证的微信号




  • Openai 的账号密码




  • 个人电脑或者一台 linux 虚拟机做服务器


与 Chatgpt 对话


首先,关于 openai 的账号如何注册此处就不过多赘述了,网络上的资料还是非常多的,xdm 实际注册的时候注意几点即可



  • 账号所选的国家尽可能选择海外,例如我注册的时候就选择了 India




  • 手机号接码的时候可能会有一定的延迟,实际操作并不是每一次都能迅速的接收到码的,来回操作了5 - 10 分钟左右收到了一个码,xdm 要耐心


chatgpt:登录地址 chat.openai.com/ 即可享受与 chatgpt 进行进行对话



实际上,ChatGPT 还可以帮我们写代码,写算法,写诗,回答最新的股票信息等等


个人微信接入ChatGPT


个人微信接入ChatGPT ,网上资料非常的多,实践了一遍之后,并不是每一个方式都可以正确运行的,或许是姿势不对,目前发现一个使用 Go 实现的项目比较香,可以非常简单快速的达到我们的目的


接入前提


先去 openai 上创建创建一个 API Keys,这个非常重要,没有这个 API Keys ,对于本案例,是没有办法接入成功的


登录 openai:beta.openai.com/login/


登录之后页面如下



可以看到 openai 的例子很多,至此的功能也是非常丰富的,感兴趣的话可以慢慢的研究,ChatGPT 也需要不断的优化和迭代



点击页面右上角的头像,进入 View API keys



创建一个新的秘钥,请自己保存好,这个秘钥相当重要,主要是用于和 openai 认证和交互的



安装部署方式


下载源码,修改配置,部署服务


可以在我们的 linux 服务器上下载项目源码,并进入源码目录,拉取项目的依赖包


git clone git@github.com:qingconglaixueit/wechatbot.git
cd wechatbot
go mod tidy

当然,这种方式是需要我们有基本的 Go 环境的, 如果不会搭建 Go 的编译环境,可以查看历史文章



源码下载后,wechatbot 目录下我们可以看到如下文件



其中配置文件是 config.dev.json,实际配置文件为config.json ,我们需要拷贝一份


cp config.dev.json config.json

里面存放了如下信息



其中重点关注 api_key 字段,填入我们之前在 openai 网站上获取的 API Keys


运行 Go 的 main.go 文件


go run main.go
// 或者在项目目录下执行 go build ,编译出可执行程序后,执行可执行程序即可

程序运行之后,可以看到出现了一个二维码,我们使用微信扫码即可



  • 此处可以使用自己的微信小号来扫码,该微信号需要个人实名认证,此处注意,扫码的微信号就是 聊天机器人



扫码成功,正常登陆之后,可以看到有正常的日志,无报错信息


此时,其他人发消息给这个扫码的微信号之后,该微信号就会智能回复了,如果是在群聊中,记得要 艾特 这个机器人


另外程序运行后,会在项目路径下生成 storage.json 文件,是一个 Cookies ,这样我们终止程序,再次启动程序的时候,就不需要我们再扫码了


当然,我们也可以直接拿到别人的可执行程序,修改配置后直接运行,也可以得到同样的效果,但是不确定 xdm 是啥时候看到的文章,可执行程序或许会用不了


但是你拿到源码你就可以自己研究,还可以做自定义的功能,Go 是跨平台的,你想生成 windows 的可执行程序或者 linux 的可执行程序都是可以的


实际上,该项目是使用了 openwechat 项目github.com/eatmoreappl…


感兴趣的童鞋,可以下载源码来读一读,代码量并不大,逻辑也很清晰明了,自然自己去从 0 到 1 写的话也是可以的,注意如下点:





  • 如何与 openai 对接,拿到相应的权限,请求响应的接口拿到我们期望的回复,可以直接查看 openai 的对接文档


接入效果


私聊效果,直接发消息即可



群聊效果,记得要 艾特这个机器人



关于 ChatGPT 的思考


ChatGPT 也还在不断的优化和迭代当中,相信未来会有更多的惊喜


诚然,在未来的发展,更加趋向于智能化,很多机械的,简单重复的工作,自然而然是会被逐步替代的,这些都是必须得直面的,无法逃避


我们更多的应该是以开放的心态,拥抱变化,向阳而生,不断的提升自己的核心竞争力,将自己作为一个产品来进行迭代和优化,将自己打造成一个伟大的产品岂不是很酷吗?


本次就是这样,如果有想体验 ChatGPT 的可以加我机器人的微信(xiaomotongneza),拉你进体验群哦,希望本文能给你带来帮助


感谢阅读,欢迎交流,点个赞,关注一波 再走吧


欢迎点赞,关注,收藏


朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力



好了,本次就到这里


技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。


我是阿兵云原生,欢迎点赞关注收藏,下次见~


作者:阿兵云原生
链接:https://juejin.cn/post/7176813187705077816
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

点兔换图——新年兔了个兔专题

前言 本篇是通过图片的点击事件去切换图片,实现图片点击轮播,而新年兔了个兔专题,当然是使用了一系列兔子的图片作为轮播图展示的,下面我们来看看怎么实现点兔换图的。 正篇 实现方法 其实安卓中实现方法很简单,我们可以轻松办到,就是ImageView中增加点击事件 ...
继续阅读 »

前言


本篇是通过图片的点击事件去切换图片,实现图片点击轮播,而新年兔了个兔专题,当然是使用了一系列兔子的图片作为轮播图展示的,下面我们来看看怎么实现点兔换图的。


正篇


实现方法


其实安卓中实现方法很简单,我们可以轻松办到,就是ImageView中增加点击事件


class RabbitFirst : AppCompatActivity() {

private lateinit var binding: ActivityRabbitFirstBinding
private var id by Delegates.notNull<Int>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityRabbitFirstBinding.inflate(layoutInflater)

setContentView(binding.root)


id = 0

binding.img1.setOnClickListener {
if (id == 0) {
binding.img1.setImageBitmap(BitmapFactory.decodeResource(resources, R.drawable.rabit_c) )
id++
return@setOnClickListener
}
if (id == 1) {
binding.img1.setImageResource(R.drawable.rabit_b)
id++
return@setOnClickListener
}
if (id == 2) {
binding.img1.setImageResource(R.drawable.rabit_a)
id++
return@setOnClickListener
}
if (id == 3) {
binding.img1.setImageResource(R.drawable.rabit_d)
id = 0
return@setOnClickListener
}

Log.i("id ===$id", "is id")
}
}
}

如果图片多了可以使用数组去存,然后单独写方法去处理,这里只有四张图,所以我这里使用if判断,主要还是没找到有关setImageResource的对应方法,网上似乎说没有对应的get方法,可以使用加setTag和getTag方法去实现,和我的判断方法也类似,我的判断方法就是如果有四张图,我们就给它显示顺序,从0-3,开始,id为0-2时点击图片切换下一张,到id=3时再清空id值,置为0,这样又能回到第一张兔子图。


展示效果


最终效果如下,我们点击图片就可以进行图片轮换:


7ff3bf7180138f1af403f321b3f84b32.gif


ps:实现的时候出现了不能点击的问题,然后发现原来是在点击事件使用id全局变量增加时正好依次增加最后还是回到原图了,所以需要if里加上返回return,不经过下个if检查


总结


虽然形式很简单,但也是安卓的实现方法去做的,其实很多五花八门的效果都是从最简单的开始,然后添加各种新的技术最终才变得更加有趣好看。


作者:ObliviateOnline
链接:https://juejin.cn/post/7197794878775787579
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

高仿B站自定义表情

在之前的文章给你的 Android App 添加自定义表情 中我们介绍了自定义表情的原理,没看过的建议看一下。这一篇文章将介绍它的应用,这里以B站的自定义表情面板为例,效果如下: 自定义表情的大小 在给你的 Android App 添加自定义表情 的文章中,...
继续阅读 »

在之前的文章给你的 Android App 添加自定义表情
中我们介绍了自定义表情的原理,没看过的建议看一下。这一篇文章将介绍它的应用,这里以B站的自定义表情面板为例,效果如下:



自定义表情的大小


给你的 Android App 添加自定义表情


的文章中,我们说过当我们写死表情的大小时,文字的 textSize 变大变小时都会有一点问题。


文字大于图片大小时,在多行的情况下,只有表情的行间距明显小于其他行的间距。如图:



为什么会出现这种情况呢?如下图所示,我在top, ascent, baseline, descent, bottom的位置标注了辅助线。



可以很清晰的看到,在只有表情的情况下,top, ascent, descent, bottom的位置有明显的问题。原因是 DynamicDrawableSpangetSize 方法里面对 FontMetricsInt 进行了修改。解决的方式很简单,就是注释掉修改代码就行,代码如下。修改后,效果如下图所示。


@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
//
// if (fm != null) {
// fm.ascent = -rect.bottom;
// fm.descent = 0;
//
// fm.top = fm.ascent;
// fm.bottom = 0;
// }

return rect.right;
}


不知道你还记不记得,我们说过getSize 的返回值是表情的宽度。上面的注释代码其实是设置了表情的高度,如果文本的大小少于表情时,就会显示不全,如下图所示:



那这种情况下,应该怎么办?这里不卖关子了,最终代码如下。解决方式非常简单就是分情况来判断。当文本的高度小于表情的高度时,设置 fmtop, ascent, descent, bottom的值,让行的高度变大的同时让大的 emoji 图片居中。


 @Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();

float drawableHeight = rect.height();
Paint.FontMetrics paintFm = paint.getFontMetrics();

if (fm != null) {
int textHeight = fm.bottom - fm.top;
if(textHeight <= drawableHeight) {//当文本的高度小于表情的高度时
//解决文字的大小小于图片大小的情况
float textCenter = (paintFm.descent + paintFm.ascent) / 2;
fm.ascent = fm.top = (int) (textCenter - drawableHeight / 2);
fm.descent = fm.bottom = (int) (textCenter + drawableHeight / 2);
}
}
return rect.right;
}


当然,你可能发现了,B站的 emoji 表情好像不是居中的。如下图所示,B站对 emoji 表情的处理类似基于 baseline 对齐。



上面最难理解的居中已经介绍,对于其他方式比如 baseline 和 bottom 就简单了。完整代码如下:


@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
Drawable d = getDrawable();
if(d == null) {
return 48;
}
Rect rect = d.getBounds();

float drawableHeight = rect.height();
Paint.FontMetrics paintFm = paint.getFontMetrics();

if (fm != null) {
if (mVerticalAlignment == ALIGN_BASELINE) {
fm.ascent = fm.top = (int) (paintFm.bottom - drawableHeight);
fm.bottom = (int) (paintFm.bottom);
fm.descent = (int) paintFm.descent;
} else if(mVerticalAlignment == ALIGN_BOTTOM) {
fm.ascent = fm.top = (int) (paintFm.bottom - drawableHeight);
fm.bottom = (int) (paintFm.bottom);
fm.descent = (int) paintFm.descent;
} else if (mVerticalAlignment == ALIGN_CENTER) {
int textHeight = fm.bottom - fm.top;
if(textHeight <= rect.height()) {
float textCenter = (paintFm.descent + paintFm.ascent) / 2;
fm.ascent = fm.top = (int) (textCenter - drawableHeight / 2);
fm.descent = fm.bottom = (int) (textCenter + drawableHeight / 2);
}
}
}

return rect.right;
}

动态表情


动态表情实际上就是 gif 图。我们可以使用 android-gif-drawable 来实现。在 build.gradle 中增加依赖:


dependencies {
...
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.25'
}

然后在我们创建自定义 ImageSpan 的时候传入参数就可以了:


val size = 192
val gifFromResource = GifDrawable(getResources(), gifData.drawableResource)
gifFromResource.stop()
gifFromResource.setBounds(0,0, size, size)
val content = mBinding.editContent.text as SpannableStringBuilder
val stringBuilder = SpannableStringBuilder(gifData.text)
stringBuilder.setSpan(BilibiliEmojiSpan(gifFromResource, ALIGN_BASELINE),
0, stringBuilder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

关于 android-gif-drawable 更具体用法可以看 Android加载Gif动画android-gif-drawable的使用


总结


核心部分的代码已经介绍了,完整代码还在整理,后面放出来。最后求一个免费的赞吧🥺


作者:小墙程序员
链接:https://juejin.cn/post/7196592276159823931
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

android 自定义view 跑马灯-光圈效果

系统: mac android studio: 4.1.3 kotlin version: 1.5.0 gradle: gradle-6.5-bin.zip 本篇效果: 前沿 最近在bilibili看到一个跑马灯光圈效果挺好, 参考着思路写了一下. bili...
继续阅读 »

系统: mac


android studio: 4.1.3


kotlin version: 1.5.0


gradle: gradle-6.5-bin.zip


本篇效果:


8140FE3CF87738708E0C5D0E4F59704F


前沿


最近在bilibili看到一个跑马灯光圈效果挺好, 参考着思路写了一下.


bilibili地址,美中不足的是这是html代码 QaQ


实现思路




  • 将效果分为3层



    • 第一层: 背景

    • 第二层: 跑马灯光圈

    • 第三层: 展示区




如图所示:


Nov-28-2022 17-19-34



tips: 图片截取自上方bilibili视频



换到android中直接将view当作背景层, 在利用Canvas绘制跑马灯层即可


将View圆角化


 // 设置view圆角
 outlineProvider = object : ViewOutlineProvider() {
   override fun getOutline(view: View, outline: Outline) {
     // 设置圆角率为
     outline.setRoundRect(0, 0, view.width, view.height, RADIUS)
  }
 }
 clipToOutline = true

这段代码网上找的,源码还没有看, 有机会再看吧.


image-20221128173221355


来看看当前效果:


CD09F6ED6DBE6895E487C703B7DB64F0


自定义跑马灯光圈


这几个字可能有点抽象,所以来看看要完成的效果:


Nov-28-2022 17-45-34


接下来只需要吧黄框外面和里面的的去掉就完成了旋转的效果:


去掉外面:


Nov-28-2022 17-47-38


去掉里面:


Nov-28-2022 17-47-32


这都是html效果,接下来看看android怎么写:


 class ApertureView @JvmOverloads constructor(
     context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : View(context, attrs, defStyleAttr) {
     companion object {
         val DEF_WIDTH = 200.dp
         val DEF_HEIGHT = DEF_WIDTH
         private val RADIUS = 20.dp
    }
 
     private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     private val rectF by lazy {
         val left = 0f + RADIUS / 2f
         val top = 0f + RADIUS / 2f
         val right = left + DEF_WIDTH - RADIUS
         val bottom = top + DEF_HEIGHT - RADIUS
         RectF(left, top, right, bottom)
    }
 
     override fun onDraw(canvas: Canvas) {
         val left = rectF.left + rectF.width() / 2f
         val right = rectF.right + rectF.width()
         val top = rectF.top + rectF.height() / 2f
         val bottom = rectF.bottom + rectF.height() / 2f
 
         // 绘制渐变view1
         paint.color = Color.GREEN
         canvas.drawRect(left, top, right, bottom, paint)
 
         // 绘制渐变view2
         paint.color = Color.RED
         canvas.drawRect(left, top, -right, -bottom, paint)
 
    }
 }

这里就是计算偏移量等,都比较简单:


542DD72464B89550F97E8BAD9EFE6FD5


因为咋们是view,并且已经测量了view的宽和高,所以超出的部分就不展示了


跑马灯动起来


这段代码比较简单,直接开一个animator即可


  private val animator by lazy {
    val animator = ObjectAnimator.ofFloat(this, "currentSpeed", 0f, 360f)
    animator.repeatCount = -1
    animator.interpolator = null
    animator.duration = 2000L
    animator
  }
 
 var currentSpeed = 0f
   set(value) {
     field = value
     invalidate()
  }
         
 override fun onDraw(canvas: Canvas) {
 
   // withSave 保存画布
   canvas.withSave {
     
   // 画布中心点旋转
   canvas.rotate(currentSpeed, width / 2f, height / 2f)
     // 绘制渐变view1 绘制渐变view2
    ...
  }
 }

14162A8D36FFE0BEB6CD9B9D5A67446F


'去掉'里面


去除里面部分有2种方式



  • 方式一: 利用 clipOutPath() 来clip掉中间区域, 这个api对版本有要求

  • 方式二: 重新绘制一个 RoundRect() 来覆盖掉中间区域


方式一:


 private val path by lazy {
     Path().also { it.addRoundRect(rectF, RADIUS, RADIUS, Path.Direction.CCW) }
 }
 
 override fun onDraw(canvas: Canvas) {
 
     // withSave 保存画布
     canvas.withSave {
       canvas.clipOutPath(path)
          // 画布中心点旋转
       canvas.rotate(currentSpeed, width / 2f, height / 2f)
       
       // 绘制渐变view1 ..view2...
    }
 }

方式二:


 override fun onDraw(canvas: Canvas) {
   // withSave 保存画布
   canvas.withSave {
 
     // 画布中心点旋转
     canvas.rotate(currentSpeed, width / 2f, height / 2f)
 
     // 绘制渐变view1
 
     // 绘制渐变view2
 
  }
 
   paint.color = Color.BLACK
   canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)
 }

来看看当前效果:


B9B3733C51780A7AFB53CBA080582B20


但是现在看起来还是有一点生硬, 可以让view渐变一下


 private val color1 by lazy {
   LinearGradient(width * 1f,height / 2f,width * 1f,height * 1f,
     intArrayOf(Color.TRANSPARENT, Color.RED), floatArrayOf(0f, 1f),
     Shader.TileMode.CLAMP
  )
 }
 
 private val color2 by lazy {
   LinearGradient( width / 2f,height / 2f,width / 2f, 0f,
     intArrayOf(Color.TRANSPARENT, Color.GREEN), floatArrayOf(0f, 1f),
     Shader.TileMode.CLAMP
  )
 }
 
 override fun onDraw(canvas: Canvas) {
 //
   canvas.withSave {
     canvas.rotate(currentSpeed, width / 2f, height / 2f)
    ...
     // 绘制渐变view1
     paint.shader = color1
     canvas.drawRect(left1, top1, right1, bottom1, paint)
     paint.shader = null
 
     // 绘制渐变view2
     paint.shader = color2
     canvas.drawRect(left1, top1, -right1, -bottom1, paint)
     paint.shader = null
  }
 
   // 中间rect
   canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint)
 }

这样一来,就更有感觉了


效果图:


FBFD3920C18DA5E6821CA08C9CFB8052


基本效果就完成了,那么如何给其他view也可以轻松的添加这个炫酷的边框呢?


很显然,view是办不到的,所以我们只能自定义viewgroup


代码没有改变,只是在自定义viewgroup时,onDraw() 不会回调, 因为viewgroup主要就是用来管理view的,所以要想绘制viewgroup最好是重写dispatchDraw()方法,


在dispatchDraw()方法中,需要注意的是 super.dispatchDraw(canvas) , 这个super中会绘制children,


所以为了避免 view被跑马灯背景覆盖,需要将super.dispatchDraw(canvas) 写到最后一行


 #ApertureViewGroup.kt
 
 override fun dispatchDraw(canvas: Canvas) {
         val left1 = width / 2f
         val top1 = height / 2f
 
         val right1 = left1 + width
         val bottom1 = top1 + width
         canvas.withSave {
             canvas.rotate(currentSpeed, width / 2f, height / 2f
             // 绘制渐变view1
             paint.shader = color1
             canvas.drawRect(left1, top1, right1, bottom1, paint)
             paint.shader = null
 
             if (mColor2 != -1) {
                 // 绘制渐变view2
                 paint.shader = color2
                 canvas.drawRect(left1, top1, -right1, -bottom1, paint)
                 paint.shader = null
            }
        }
 
         paint.color = mMiddleColor
         canvas.drawRoundRect(rectF, mBorderAngle, mBorderAngle, paint)
 
 // 一定要写到最后一行,否则children会被跑马灯覆盖掉
         super.dispatchDraw(canvas)
    }

最后在调用的时候直接:


 <ApertureViewGroup
     android:layout_width="200dp"
     android:layout_height="200dp"
 
     // 边框颜色
     android:background="@color/cccccc"
                                                       
 // 边框宽度                                            
     app:aperture_border_width="50dp"
                       
 // 边框角度
     app:aperture_border_angle="20dp"                                               
 
 // 渐变颜色1
     app:aperture_color1="@color/purple_200"
                                                                                               
 // 渐变颜色2 如果不写,默认只有一个渐变在跑马灯
     app:aperture_color2="@color/color_FFC107"
                                                       
 // 旋转时间
     app:aperture_duration="3000"
                                                       
 // 中间空心颜色
     app:aperture_middle_color="@color/white">
 
     <XXXX View
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:gravity="center" />
 </com.example.customviewproject.f.f2.ApertureViewGroup>

本篇代码比较简单,不过这个思路确实挺好玩的!


最终效果:


A051CC6A0481AE320B2371E271889D04


完整代码


作者:史大拿
链接:https://juejin.cn/post/7171030095866363934
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

谷歌版ChatGPT首秀,第一个Demo就大翻车,市值暴跌7000亿

现在看来,拼第一枪,微软赢了。如今的谷歌,有点秦失其鹿的味道。在微软更新 ChatGPT 加持的必应搜索之后,大家都极为关注谷歌 Bard 的首秀。怎知近日曝出的 Demo 大翻车,导致股票一夜之间暴跌 7000 亿人民币。而看微软那边,风景独好。前后脚发布新...
继续阅读 »

现在看来,拼第一枪,微软赢了。

如今的谷歌,有点秦失其鹿的味道。

在微软更新 ChatGPT 加持的必应搜索之后,大家都极为关注谷歌 Bard 的首秀。怎知近日曝出的 Demo 大翻车,导致股票一夜之间暴跌 7000 亿人民币。而看微软那边,风景独好。


前后脚发布新一代 AI 搜索,人们的期待却完全反过来,不知重新来过谷歌会不会还抢跑。

昨晚,谷歌举行了「Google presents : Live from Paris」大会。大家都在期待 Bard 的首秀,结果却令观众大失所望。基本没有多少 Bard 的展示内容。

在展示增强现实搜索功能时,演讲人还把演示 Demo 的手机弄丢了,引来人们尴尬而不失礼貌的笑。不过谷歌在活动中仍然通过全面的多模态搜索能力展现了自己的实力。


图:昨晚令人失望的发布会

但发布会过后,人们回过味来:我们现在要的是 ChatGPT 搜索。Bard 是现在在科技界爆红、给谷歌搜索带来巨大威胁的 ChatGPT 的竞品,备受期待的 Bard 却出师不利。

也许,这也是谷歌股价今天暴跌的主要原因。

谷歌 Bard 首秀 Demo 翻车

谷歌在广告中表示,Bard 是一项实验性对话式 AI 服务,由 LaMDA 模型提供支持。Bard 使用谷歌的大型语言模型构建,并利用网络信息获得知识,因为模型体量相对较小,所以需要的算力更小,这意味着能够服务更多的人。谷歌将其聊天机器人描述为「好奇心的发射台」,有助于简化复杂的话题。

但 AI 回答问题也要有准确性,谷歌 Bard 翻车在哪里?仅仅是一个事实错误。

谷歌 Demo 的一 GIF 显示,在回答问题「关于詹姆斯韦伯太空望远镜(JWST)有哪些新发现,我可以告诉我 9 岁孩子哪些内容?」Bard 提供了三个要点,其中一个指出「该望远镜拍摄了太阳系外行星的第一张照片。」


这就是 Bard 的首秀,包含一个事实错误。

然而,推特上的一些天文学家指出这是不正确的,第一张系外行星图像是在 2004 年拍摄的。

天体物理学家 Grant Tremblay 在推特上写道:「我相信 Bard 的表现会令人印象深刻,但郑重声明:JWST 并没有拍下我们太阳系外行星的第一张图片。」


加州大学圣克鲁兹分校天文台主任 Bruce Macintosh 也指出了这个错误。「作为一个在 JWST 发射前 14 年拍摄系外行星的人,感觉你应该找到一个更好的例子?」

在跟进的推文中,Tremblay 补充说:「我非常喜欢并感谢地球上最强大的公司之一正在使用 JWST 搜索来宣传他们的大语言模型。非常棒!但是 ChatGPT 这些模型虽然令人印象深刻,但经常出错,还非常自信。看到大模型进行自我错误检查的未来将会很有趣。」

正如 Tremblay 所指出的,ChatGPT 和 Bard 等 AI 聊天机器人的一个主要问题是它们会一本正经的胡说八道。这些系统经常「产生幻觉」—— 即编造信息 —— 因为它们本质上是自动生成系统。

当前的 AI 不是查询已证实事实的数据库来回答问题,而是接受大量文本语料库的训练并分析模式,以推定任何给定句子中的下一个单词出现的概率。换句话说,它们是统计性的,而不是确定性的 —— 这一特征导致一些 AI 学者将它们称为「废话生成器」。

当然,互联网上已经充斥着虚假和误导性信息,但微软和谷歌希望将这些工具用作搜索引擎,这可能会使问题更加复杂。在搜索引擎上,聊天机器人的回答几乎会被认为是全知机器的权威答案。

微软昨天演示了全新人工智能必应搜索引擎,试图通过用户条款免责来规避这些问题。「必应由 AI 提供支持,因此可能会出现意外和错误,」该公司的免责声明称。「确保检查事实并分享反馈,以便我们学习和改进。」

谷歌发言人告诉媒体:「这凸显了严格测试过程的重要性,我们本周将通过 Trusted Tester 计划启动这一过程。我们会将外部反馈与我们自己的内部测试相结合,以确保 Bard 的回应符合现实世界信息的质量、安全性和接地性的高标准。」

亲身体验微软新必应:升级 ChatGPT 的第一步

那边谷歌 Bard 首秀翻车,这边有人给出了微软新必应 AI 功能的详细使用报告。ChatGPT 驱动的新版本必应能够生成搞笑的辞职信、当下新闻事件以及错误的信息。那么它的搜索功能和效果到底怎样呢?

昨日, The Verge 花了几个小时在微软的 Edge 浏览器中使用这个新版本必应和其他 AI 功能,并感觉到了对 ChatGPT 的重大升级。

体验者问 Edge 全新「撰写」功能的第一个问题:用幽默诙谐的语气向老板写一封辞职信,原因是他觉得自己的工作正在被 AI 取代。结果显示,该功能生成的辞职信令人印象深刻,而且真的很有趣:

Subject: I quit!

Dear Boss,

I regret to inform you that I am resigning from my position as a data analyst, effective immediately. The reason is simple: Al is replacing my job.

Yes, I know you told me that Al is just a tool to help me work taster and smarter, but I don’t buy it. I’ve seen the way you look at that shiny new machine in the corner, with its blinking lights and soothing voice.

I’ve heard the rumors that it can do everything I can do, but better, cheaper, and without complaining. I’ve felt the cold shoulder you’ve given me ever since you installed it.

So, I’ve decided to save you the trouble of firing me and just quit. I don’t want to compete with a machine that can crunch numbers, generate reports, and make witty jokes at the same time.

I don’t want to work for a boss who values Al over human intelligence and creativity. I don’t want to be part of a company that is willing to sacrifice its employees for the sake of efficiency and profit.

I’m sure you’ll be very happy with your new Al assistant. I hope it doesn’t turn against you and take over the world.

I hope it doesn’t make any mistakes that cost you your reputation and customers. I hope it doesn’t get bored and start playing games with your data. I hope it doesn’t develop a personality and fall in love with you.

But if it does, don’t come crying to me. I’ll be too busy enjoying my life, pursuing my passions, and finding a new job that appreciates me for who I am.

Goodbye and good luck,

Your former data analyst

体验者在 ChatGPT 中尝试过同样的询问,但它从未创建过像这样有趣和活泼的答案。然而一封有趣的辞职信并不会从根本上将微软的 AI 功能与 ChatGPT 区分开来,但微软 AI 模型的工作方式与 ChatGPT 存在一些更大的差异。

首先,微软正在将实时新闻输入其聊天版必应,让你可以询问刚刚发生或正在发生的事件。体验者问它「微软在 Bing AI 活动期间宣布了什么?」,它根据涵盖微软 AI 驱动的必应相关公告的多份资料创作了一份新闻摘要。那仅仅是在微软正式宣布这一消息几分钟后。


但与 ChatGPT 非常相似,新必应并不总是准确。当必应出现问题时,旁边会有一个「dislike」按钮,这个按钮会将有关查询的反馈和答案发送给微软。

微软还将聊天答案与必应中的传统搜索结果一起显示出来,并通过常用链接找到答案。必应和 ChatGPT 之间的最大区别在于微软将这些聊天机器人的功能集成到其 Edge 浏览器中。Edge 现在有一个侧边栏,可以扫描整个网页并允许你挑选信息或对网站运行聊天查询。


在这个新的侧边栏中甚至还有一个组合选项卡,可以让输入参数变得更容易一些。这一选项卡为用户提供快速选项来指定语气、格式和长度。你可以要求微软 AI 模型在这里写任何东西,它可以直接被用于博客文章、电子邮件或简单的列表。

你可以想象未来 Word 或 Outlook 有类似的集成来让你创建文档或电子邮件。从技术上讲,现在只需在这个新的 Edge 边栏旁加载基于 Web 的 Word 版本,就可以做到这一点。


微软表示,全新 AI 加持的必应应该比 ChatGPT 更擅长编写代码,很快就会有开发人员测试必应的编程能力。

当然,就像人们试图找到 ChatGPT 的缺陷一样,一定会有成千上万的人想要破解新的必应。微软表示他们有一些额外的保护措施来避免这种情况。

无论如何,微软都非常大胆地在自己的搜索引擎中向公众开放类似于 ChatGPT 的 AI 助手功能。微软的目标显然是抢走谷歌在搜索引擎领域的一部分市场份额,让必应更强大,每个人都在等着看谷歌如何回应。

谷歌虽然推出了对标 ChatGPT 的 Bard,但从这两天业界和机器学习社区的反应看,谷歌似乎不敌微软。现在,谷歌又在自己 Bard 首秀的演示视频中翻车了。

微软认为他们正在掀起搜索引擎的新一轮变革,而谷歌处于被动状态。微软能否撼动谷歌在搜索引擎领域的霸主地位,仍未可知。

参考内容:

https://www.theverge.com/2023/2/8/23590864/google-ai-chatbot-bard-mistake-error-exoplanet-demo

https://www.theverge.com/2023/2/8/23590873/microsoft-new-bing-chatgpt-ai-hands-on

来源:mp.weixin.qq.com/s/1mkAlJbtYCmQcz_mV9cdoA

收起阅读 »

一个大龄小前端的年终悔恨

今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧仔细想了想今年也没有做什么呀! 真是年纪越大时间越快 为什么有大有小啊?95的够大了吧步入前端也才不到3年So一个大龄的小前端技术有长进么?一个PC端项目 用了 react antd redux...
继续阅读 »

今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧

仔细想了想今年也没有做什么呀! 真是年纪越大时间越快 

为什么有大有小啊?

95的够大了吧

步入前端也才不到3年

So一个大龄的小前端

技术有长进么?

一个PC端项目 用了 react antd redux-toolkit react-router ahooks axios 也就这样吧,就一点简单的项目,react熟练了么?有点会用了,可是我工作快3年了,写项目还是要来回查文档,antd用的熟练的时候倒是可以不用去查文档,可是过了就忘了,今天写项目就有点想不起来怎么用了,查了文档才可以继续写下去

有长进么?
  1. react熟练了一些,可以自己看源码了

  2. 自己解决问题的能力有了一点提升

  3. 技术的广度认识有了(23年目标是深度)

  4. 数据结构了解一点了 二叉树 队列 链表 队列 (还学了一点算法,不过忘了🤣)

  5. 写代码喜欢封装组件了

  6. node学了一点又忘了

  7. ts会的多了一点

  8. antd也好一点了,以前在群里问一些小白问题,还好有个大哥经常帮我

  9. css 还是不咋地 不过我刚买了一个掘金小册 [s.juejin.cn/ds/hjUap4V

生活上有什么说的呢?

生活很好 吃喝不愁

就是太久没有回家了 老家黑龙江 爷爷奶奶年纪大了 有时候想不在杭州了 回哈尔滨吧 这样可以多陪陪他们 可是回哈尔滨基本就是躺平了 回去我能做什么? 继续做前端? 好好补补基础去做一个培训讲师?

回去的好处是房子压力小 可以买一个车 每天正常上班 下班陪家人 到家有饭吃 想想也挺好

不过女朋友想在杭州,所以我还会在杭州闯一下的,毕竟我们在杭州买房子也是可以努力一下的

女朋友对我很好 我们在一起也快3年了 我刚步入前端的时候我们刚在一起 2020-05-20 她把我照顾的很好 她很喜欢我我感觉的到 我平时不太会表达 其实我是想跟她结婚的我也喜欢她 我对她耐心少了一点 这一点我会改的 以后我想多跟她分享我每天发生的事 我想这样她会更开心一点吧

今年她给我做了好多的饭,有段时间上班都是她晚上下班回来做的(她下班的早 离家近) 第二天我们好带去(偶尔我们吃一段时间的轻食) 可是我还是胖了



2023要怎么做?

我想成为大佬 我想自律一些 还有工资也要多一点吧
  • 开年主要大任务 两个字 搞钱 咱们不多来 15万可以吧 嗯 目标攒15W

  • 紧接上条 要是买 20W-30W的车 那你可以少攒点 8万到10万 (买车尽量贷款10W)

  • MD 减肥可以吧 你不看看你多胖了呀 175的身高 快170斤了减到140斤 (总觉得不胖,壮)

  • 技术一定要提升 你不能再这样下去了 要被清除地~


技术我们来好好的捋一下,该怎么提升

  1. 现有项目自己codeReview(改改你的垃圾代码吧)

  2. css多学点

    1. css in js

    2. Tailwindcss

    3. css Module less 写法好好研究一下

    4. css 相关配置要会

  3. react源码要搞一下

    1. fiber

    2. hooks

    3. diff

    4. 一些相关的库的源码 (router,redux等)

  4. webpack vite (要能写出来插件)

  5. node 这个一定要学会 (最起码能自己写接口和工具)

  6. 文章要搞起来 (最起码要写20篇,前5篇要一周一篇文章)

2023 搞一个 pc端 H5 小程序 后台接口 要齐全 必须搞出来一个 加油💪🏻

作者:奈斯啊小刘超奈斯_
来源:juejin.cn/post/7174789490580389925

收起阅读 »

老板说:把玉兔迎春图实现高亮

web
前言兔年来临,老板意气风发的说:我们的系统登录页要换做玉兔迎春的背景页,而且用户ctrl+f搜索【玉兔迎春】关键字时,图片要高亮。新的一年,祝大家身体健康、Bug--一、明确需求将系统的登录页面背景换做如上图【玉兔迎春】。而且,用户可以通过搜索关键字【玉兔迎春...
继续阅读 »

前言

  • 兔年来临,老板意气风发的说:我们的系统登录页要换做玉兔迎春的背景页,而且用户ctrl+f搜索【玉兔迎春】关键字时,图片要高亮。


新的一年,祝大家身体健康、Bug--


一、明确需求

将系统的登录页面背景换做如上图【玉兔迎春】。

而且,用户可以通过搜索关键字【玉兔迎春】让背景图的文字进行高亮。

下面我们进行分析一下。

二、进行分析

接到该需求的时候,心里是这样子的。


于是,老板像是看穿我的疑惑时,语重心长的对我们说:我们要给用户一个焕然一新的感觉。

疯狂点点头,并想好如何让图片里面的文字进行高亮的对策。

静下来思考片刻,其实不是很难。

2.1 思路

我们只需要盖一层div在图片上,然后设置文字透明,浏览器ctrl+f搜索的时候,会给文字他高亮黄的颜色,我们就可以看到文字了。

盖的这层div,里面包含着我们的文字。


那么,难点就是怎么从图片获取文字出来。

其实这个技术,有个专业词语来描述,叫ocr识别技术。

2.2 ocr

ocr,其实也叫“光学字符识别技术”,是最为常见的、也是目前最高效的文字扫描技术,它可以从图片或者PDF中识别和提取其中的文字内容,输出文本文档,方便验证用户信息,或者直接进行内容编辑。

揭秘该技术:实现文字识别?从图片到文字的过程发生了什么?


分别是输入、图像与处理、文字检测、文本识别,及输出。每个过程都需要算法的深度配合,因此从技术底层来讲,从图片到文字输出,要经历以下的过程:

1、图像输入:读取不同图像格式文件;

2、图像预处理:主要包括图像二值化,噪声去除,倾斜校正等;

3、版面分析:将文档图片分段落,分行;

4、字符切割:处理因字符粘连、断笔造成字符难以简单切割的问题;

5、字符特征提取:对字符图像提取多维特征;

6、字符识别:将当前字符提取的特征向量与特征模板库进行模板粗分类和模板细匹配,识别出字符;

7、版面恢复:识别原文档的排版,按原排版格式将识别结果输出到文本文档;

8、后处理校正: 根据特定的语言上下文的关系,对识别结果进行校正。

2.3 应用

随着ocr技术的成熟,不少软件已经出了该功能。

比如:微信、qq、语雀等等。

还有一些试卷试题,都会用到ocr识别技术。


还有一些技术文档,实现自定义搜索功能,表格关键字高亮。


老板这次需求:把玉兔迎春图实现高亮。

和如上实现的技术思路类似。

我们也可以自定义颜色,加个span标签给其想要的样式。

三、使用

当然,我们可能并不关心底层的实现,只关心怎么怎么去使用。

我们可以调用百度API:文字提取技术

还可以使用java的tesseract-ocr库,其实就是文字的训练。

所以会有个弊端,就是文件可能会有点大,存放着大量文字。

后记

在一个需求的产生之后,我们如果没什么思路,可以借鉴一下,目前市场上有没有类似的技术的沉淀,从而实现需求。

最后,望大家的新的一年,工作顺利,身体健康。

玉兔迎春啦🐇🧨🐇🏮🐇~

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

作者:Dignity_呱
来源:juejin.cn/post/7186459084303335481

收起阅读 »

一个有趣的交互效果的实现

web
效果分析最近在做项目,碰到了这样一个需求,就是页面有一个元素,这个元素可以在限定的区域内进行拖拽,拖拽完成吸附到左边或者右边,并且在滚动页面的时候,这个元素要半隐状态,停止滚动的时候恢复到原来的位置。如图所示:根据视频所展示的效果,我们得出了我们需要实现的效果...
继续阅读 »

效果分析

最近在做项目,碰到了这样一个需求,就是页面有一个元素,这个元素可以在限定的区域内进行拖拽,拖拽完成吸附到左边或者右边,并且在滚动页面的时候,这个元素要半隐状态,停止滚动的时候恢复到原来的位置。如图所示:


根据视频所展示的效果,我们得出了我们需要实现的效果主要有2个部分:

  • 拖拽并吸附

  • 滚动半隐元素

那么如何实现这2个效果呢?我们一个效果一个效果的来分析。

ps: 由于这里采用的是react技术栈,所以这里以react作为讲解

首先对于第一个效果,我们要想实现拖拽,有2种方式,第一种就是html5提供的拖拽api,还有一种就是监听鼠标的mousedown,mousemove和mouseup事件,由于这里兼容的移动端,所以我采用的是第二种实现方法。

思路是有了,接下来我想的就是将这三个事件封装一下,写成一个hook函数,这样方便调用,也方便扩展。

对于拖拽的实现,我们只需要在鼠标按下的时候,记录一下横坐标x和纵坐标y,在鼠标拖动的时候用当前拖动的横坐标x和横坐标y去与鼠标按下的时候的横坐标x与y坐标相减就可以得到拖动的偏移坐标,而这个偏移坐标就是我们最终要使用到的坐标。

在鼠标按下的时候,我们还需要减去元素本身所在的left偏移和top偏移,这样计算出来的坐标才是正确的。

然后,由于元素需要通过设置偏移来改变位置,因此我们需要将元素脱离文档流,换句话说就是元素使用定位,这里我采用的是固定定位。

hooks函数的实现

基于以上思路,一个任意拖拽功能实现的hooks函数就结构就成型了。

当然由于我们需要限定范围,这时候我们可以思考会有2个方向上的限定,即水平方向和垂直方向上的限定。除此之外,我们还需要提供一个默认的坐标值,也就是说元素默认应该是在哪个位置上。现在我们用伪代码来表示一下这个函数的结构,代码如下:

const useLimitDrag = (el,options,container) => {
   //核心代码
}
export default useLimitDrag;

参数类型

这个hooks函数有3个参数,第一个参数自然是需要拖拽的元素,第二个参数则是配置对象,而第三个参数则是限定的容器元素。拖拽的元素和容器元素都是属于dom元素,在react中,我们还可以传递ref来表示一个dom元素,所以这两个参数,我们可以约定一下类型定义。我们先来定义元素的类型如下:

export type ElementType = Element | HTMLElement | null;

dom元素的类型就是Element | HTMLElement这2个类型,现在我们知道react的ref可以传递dom元素,并且我们还可以传入一个函数当作参数,所以基于这个类型,我们又额外的扩展了参数的类型,也方便配置。让我们继续写下如下代码:

import type { RefObject } from 'react';
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;

这样el和container元素的类型就一目了然,我们再定义一个类型简单合并一下这两个类型,代码如下:

export type ParamType = RefElementType | FunctionElementType;

接下来,让我们看配置对象,配置对象主要有2个地方,第一个就是默认值,第二个则是限定方向,因此我们约定了3个参数,islimitX,isLimitY,defaultPosition,并且配置对象都应该是可选的,我们可以使用Partial内置泛型将这个类型包裹一下,ok,来看看代码吧。

export type OptionType = Partial<{
   isLimitX: boolean,
   isLimitY: boolean,
   defaultPosition: {
       x: number,
       y: number
  }
}>;

嗯现在,我们可以修改一下以上的核心函数了,代码如下:

const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType)  => {
   //核心代码
}
export default useLimitDrag;

返回值类型

下一步,我们需要确定我们返回的值,首先肯定是当前被计算出来的x和y坐标,其次由于我们这个需求还有一个吸附效果,这个吸附效果是什么意思呢?就是说,以屏幕的中间作为划分界限为左右两部分,当拖动的x坐标大于中间,那么就吸附到最右边,否则就吸附到最左边。

根据这个需求,我们可以将坐标分为最大x坐标,最小x坐标以及中间的x坐标,当然由于需求只提到了水平方向上的吸附,垂直方向上并没有,但是为了考虑扩展,与之对应的我们同样要分成最大y坐标,最小y坐标以及中间的y坐标。

最后,我们还可以返回一个是否正在拖动中,方便我们做额外的操作。根据描述,以上的代码我们也就可以构造如下:

export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
//
const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType): PositionType  => {
   //核心代码
}
export default useLimitDrag;

核心代码实现第一步---判断当前环境

最基本的结构搭建好了,接下来第一步,我们要做什么?首先当然是判断当前环境是否表示移动端啊。那么如何判断呢?浏览器提供了一个navigator对象,通过这个对象的userAgent属性我们就可以判断,这个属性是一个很长的字符串,但是我们可以从其中一些值看出一些端倪,在移动端的环境中,通常都会看到iPhone|iPod|Android|ios这些字符串值,比如在iphone手机中就会有iPhone字符串,同理android也是。所以我们就可以通过写一个正则表达式来匹配这些字符串,如果有这些字符串就代表是移动端环境,否则就是pc浏览器环境,代码如下:

const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);

我们为什么要判断是否是移动端环境?因为在移动端环境,我们通常监听的是触摸事件,即touchstart,touchmove与touchend,而非mousedown,mousemove和mouseup。所以下一行代码自然就是定义好事件呢。如下:

const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];

核心代码实现第二步---一些初始化工作

下一步,我们通过useRef方法来存储拖拽元素和限定拖拽容器元素。代码如下:

const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();

接着我们获取配置对象的值,然后我们定义最大边界的值,代码如下:

const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}

随后,我们用一个变量代表鼠标是否按下的状态,这样做的目的是让拖拽变得更丝滑流畅一些,而不容易出问题,然后我们用useState定义返回的值,再定义一个对象存储鼠标按下时的x坐标和y坐标的值。代码如下:

let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}

另外为了确保拖动在限定区域内,我们需要设置滚动截断的样式,让元素不能在出现滚动条后还能拖动,因为这样会出现问题。我们定义一个方法用来设置,代码如下:

const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}

这个方法也就比较好理解了,如果使用的时候传入isLimitX那么就设置overflowX为hidden,否则不设置,y方向同理。

核心代码的实现第三步---监听事件

接下来,我们在react的钩子函数中监听事件,此时有了一个选择就是钩子函数我们使用useEffect还是useLayoutEffect呢?要决定使用哪个,我们需要知道这两个钩子函数的区别,这个超出了本文范围,不提及,可以查阅相关资料了解,这里我选择的是useLayoutEffect。

在钩子函数的回调函数中,我们首先将拖拽元素和容器元素存储下来,然后如果拖拽元素不存在,我们就不执行后续事件,回调函数返回一个函数,在该函数中我们移除对应的事件。代码如下:

useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);

核心代码实现第四步---拖动开始事件回调

接下来,我们来看一下onStartHandler函数的实现,在这个函数中,我们主要其实就是存储按下时候的坐标值,并且设置状态以及拖拽元素的鼠标样式和滚动截断的样式,随后当然是监听拖动和拖动结束事件,代码如下:

const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);

pc端是可以直接从事件对象中拿出来坐标,可是在移动端我们要通过一个changedTouches属性,这个属性是一个伪数组,第一项就是我们要获取到的坐标值。

接下来就是拖动事件的回调函数以及拖动结束的回调函数的实现了。

核心代码实现第五步---拖动事件回调

这是一个最核心实现的回调,我们在这个函数当中是要计算坐标的,首先当然是根据isStart状态来确定是否执行后续逻辑,其次,还要获取到当前拖拽元素,因为我们要根据这个拖拽元素的宽高做坐标的计算,另外还要获取到容器元素,如果没有提供容器元素,那么就是我们最开始定义的globalWidthHeight中取,然后获取鼠标按下时的x和y坐标值,将当前移动的x坐标和y坐标分别与按下时相减,就是我们的移动x坐标和y坐标,如果有设置isLimitX和isLimitY,我们还要额外设置滚动截断样式,并且我们通过将0和moveX以及最大值(也就是屏幕或者是容器元素的宽高减去拖拽元素的宽高)得到我们的最终的moveX和moveY值。

最后,我们将最终的moveX和moveY用react的状态存储起来即可。代码如下:

const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement ||globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);

核心代码实现第六步--拖动结束回调

最后在拖动结束后,我们需要重置我们做的一些操作,比如样式溢出截断,再比如移除事件的监听,以及恢复鼠标的样式等。代码如下:

const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);

到此为止,我们的第一个效果核心实现就已经算是完成大半部分了,最后我们再把需要用到的状态值返回出去。代码如下:

return {
...position,
isMove
}

合并以上的代码,就成了我们最终的hooks函数,代码如下:

import { useState, useCallback, useLayoutEffect, useRef } from 'react';
import type { RefObject } from 'react';

export type ElementType = Element | HTMLElement | null;
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;
export type ParamType = RefElementType | FunctionElementType;
export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
export type OptionType = Partial<{
isLimitX: boolean,
isLimitY: boolean,
defaultPosition: {
x: number,
y: number
}
}>;
const useAnyDrag = (el: ParamType, option: OptionType = { isLimitX: true, isLimitY: true,defaultPosition:{ x:0,y:0 } }, containerRef?: ParamType): PositionType => {
const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];
const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();
const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}
let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}
const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}
const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);
const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement || globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);
const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);
useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);
return {
...position,
isMove
}
}
export default useAnyDrag;

接下来我们来看第二个效果的实现。

半隐效果的实现分析

第二个效果实现的难点在哪里?我们都知道监听元素的滚动事件可以知道用户正在滚动页面,可是我们并不知道用户是否停止了滚动,而且也没有相关的事件或者是API能够让我们去监听用户停止了滚动,那么难点就在这里,如何知道用户是否停止了滚动。

要解决这个问题,我们还得从滚动事件中作文章,我们知道如果用户一直在滚动页面的话,滚动事件就会一直触发,假设我们在该事件中延迟个数百毫秒执行某个操作,是否就代表用户停止了滚动,然后我们可以执行相应的操作?

幸运的是,我从这里找到了答案,还真的是这么做。

如此一来,这个效果我们就实现了一大半了,我们实现一个useIsScroll函数,然后返回一个布尔值代表用户是否正在滚动和停止滚动两种状态,为了完成额外的操作,我们还可以返回一个用户滚动停止时的当前元素距离文档顶部的一个距离,也就是scrollTop。

如此一来,这个函数的实现就比较简单了,还是在react钩子函数中监听滚动事件,然后执行修改状态值的操作。但是现在还有一个问题,那就是我们如何去存储状态?

核心代码实现第一步--解决状态存储的响应式

如果使用useState来存储的话,似乎并没有达到响应式,好在react还提供了一个useRef函数,这个是一个响应式的,我们可以基于这个hooks函数结合useReducer函数来封装一个useGetSet函数,这个函数也在这里有总结到,感兴趣的可以去看看。

这个函数的实现其实也不难,主要就是利用useReducer的第二个参数强行去更新状态值,然后返回更新后的状态值。代码如下:

export const useGetSet = <T>(initialState:T):[() => T,(s:T) => void] => {
const state = useRef<T>(initialState);
const [,update] = useReducer(() => Object.create(null),{});
const updateState = (newState: T) => {
state.current = newState;
update();
}
return useMemo(() => [
() => state.current,
updateState
],[])
}

核心代码实现第二步--构建hooks函数

接下来我们来看这个hooks函数,很明显这个hooks函数有2个参数,第一个则是监听滚动事件的滚动元素,第二个则是一个延迟时间。滚动元素的类型与前面的拖拽函数保持一致,我们来详细看看。

const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
//核心代码
}

需要注意这里设置了默认值,el默认值时window对象,throlleTime默认值时300

接下来我们就是使用useSetGet方法存储状态,定义一个timer用于延迟函数定时器,然后监听滚动事件,在事件的回调函数中执行相应的修改状态值的操作,最后就是返回这两个状态值即可。代码如下:

const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
const [isScroll,setIsScroll] = useGetSet(false);
const [scrollTop,setScrollTop] = useGetSet(0);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const onScrollHandler = useCallback(() => {
setIsScroll(true);
setScrollTop(window.scrollY);
if(timer.current){
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setIsScroll(false);
},throlleTime)
},[])
useLayoutEffect(() => {
const ele = typeof el === 'function' ? (el as () => T)() : el;
if(!ele){
return;
}
ele.addEventListener('scroll',onScrollHandler,false);
return () => {
ele.removeEventListener('scroll',onScrollHandler,false);
}
},[]);
return {
isScroll: isScroll(),
scrollTop: scrollTop()
};
}

整个hooks函数代码实现起来简单明了,所以也没什么难点,只要理解到了思路,就很简单了。

两个hooks函数的使用

核心功能我们已经实现了,接下来使用起来也比较简单,样式代码如下:

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,html {
height: 100%;
}
body {
overflow:auto;
}
.App {
position: relative;
}
.overHeight {
height: 3000px;
}
.drag {
position: fixed;
width: 150px;
height: 150px;
border: 3px solid #2396ef;
background: linear-gradient(135deg,#efac82 10%,#da5b0c 90%);
z-index: 2;
left: 0;
top: 0;
}
.transition {
transition: all 1.2s ease-in-out;
}

组件代码如下:

import useAnyDrag from "./hooks/useAnyDrag";
import './App.css';
import useIsScroll from "./hooks/useIsScroll";
import { createRef } from "react";

const App = () => {
   // 这里是使用核心代码
  const { x, y, isMove, centerX, minX, maxX } = useAnyDrag(() => document.querySelector('#drag'));
  //这里是使用核心代码    
  const {isScroll} = useIsScroll();
  const scrollElement = createRef<HTMLDivElement>();
  const getLeftPosition = () => {
     if (!x || !centerX || isMove) {
        return x;
    }
     if (x <= centerX) {
        return minX || 0;
    } else {
        return maxX;
    }
  }
  const scrollPosition = () => {
     if (typeof getLeftPosition() === 'number') {
        if (getLeftPosition() === 0) {
           return -((scrollElement.current?.offsetWidth || 0) / 2);
        } else {
           return getLeftPosition() as number + (scrollElement.current?.offsetWidth || 0) / 2;
        }
    }
     return 0;
  }
  return (
     <div className="App">
        <div className="overHeight"></div>
        <div className={`${ isScroll ? 'drag transition' : 'drag'}`}
           style={{ left: (isScroll ? scrollPosition() : getLeftPosition()) + 'px', top: y + 'px' }}
           id="drag"
           ref={scrollElement}
        ></div>
     </div>
  )
}
export default App;

结语

经过以上的分析,我们就完成了这样一个需求,感觉实现完了之后,还是收获满满的,总结一下我学到了什么。

  1. 拖拽事件的监听以及拖拽坐标的计算

  2. 滚动事件的监听以及react响应式状态的实现

  3. 移动端环境与pc环境的判断

  4. 如何知道用户停止了滚动

本文就到此为止了,感谢大家观看,最后贴一下在线demo如下所示。

code.juejin.cn/pen/7164578…

作者:夕水
来源:juejin.cn/post/7163153386911563813

收起阅读 »

十分钟利用环信WebIM-vue3-Demo,打包上线一个即时通讯项目【含音视频通话】

这篇文章无废话,只教你如果接到即时通讯功能需求,十分钟利用环信WebIM-vue3-Demo,打包上线一个即时通讯项目【包含音视频通话功能】。写这篇文章是因为,结合自身情况,以及所遇到的有同样情况的开发者在接到即时通讯(IM)需求时,需要花费大量时间去熟悉相关...
继续阅读 »

这篇文章无废话,只教你如果接到即时通讯功能需求,十分钟利用环信WebIM-vue3-Demo,打包上线一个即时通讯项目【包含音视频通话功能】。


写这篇文章是因为,结合自身情况,以及所遇到的有同样情况的开发者在接到即时通讯(IM)需求时,需要花费大量时间去熟悉相关SDK厂商提供的API接口,并且需要结合自己的项目需求,在紧张张的项目工期压力之下去进行适应性调整,非常的痛苦,“本着轮子我来造,代码大家粘”的理念。

在去年的五月份我使用vue3+element plus,集成环信了web端4.xSDK,以产品化的目标,利用业余时间陆陆续续独立完成了一个完整的开源Demo,同时也在十月份以组件的形式将音视频功能也加入了进去,已经经过了测试人员测试,目前此项目已经合并进入了环信官方开源vue-demo分支,我会在之后持续提PR增加新功能,修复老的Bug,同时也欢迎大家提PR完善此开源Demo。

场景适用

提及场景适用,是因为个人认为有几个场景在小改动的情况下,能够快速使用,其余个性化较强的需求,如果使用这个demo确实需要花费点时间做些改动。

· 通用社交聊天(类似微信)
· 客服坐席沟通
· 后台内部沟通

效果预览
l 登录页



l 会话聊天页





l 联系人页



直奔主题
· 代码下载地址 https://github.com/easemob/webim-vue-demo/tree/demo-vue3


1. 下载完Demo代码,按照README.md指引先把项目启动起来,具体要求的node版本,以及目录结构一些注意事项一定要耐下性子去看README.md,能运行起来才有接下来的事情。

2. 和前端集成一些其他三方插件一样,我们首先要在平台进行注册以及去创建一个唯一的appid,只不过这个概念在环信IM的名词为appKey,同样我们先去平台进行注册,这是教你注册的文档入口(http://docs-im-beta.easemob.com/product/enable_and_configure_IM.html)

3. 打开下载好的项目代码,在 src / IM / initwebsdk.js 下去将自己注册并创建的appKey替换为自己的,Demo里的是默认的有限制,自己的项目必须改为自己的。


4. 创建一个测试ID,为后续验证使用自己创建的appKey进行登录做准备,创建方式为在环信后台管理里面进行创建。



5. 将Demo手机号验证码改为刚才注册的测试ID,以及密码登陆。代码所在路径为 src/views/Login/components/LoginInput

LoginInput组件下,在loginValue中增加两个变量名,这里我加了username以及password,并在loginIM方法中,将SDK登录方式解开注释,注释原有手机号将验证码的登录方式,修改template中输入框的双向绑定值,将其指向为username,password。




保存并重新运行项目,输入刚才注册的ID,以及密码,点击登录观察是否正常跳转,如果正常跳转则说明已经替换完成。

至此已经完成了项目配置的完全替换,即可基于此项目进行已有结构进行二次开发。

文中所提及地址导航:


· Demo示例预览地址:
https://webim-vue3.easemob.com/login

· 开源代码下载地址: https://github.com/easemob/webim-vue-demo/tree/demo-vue3

· 环信开发文档地址API文档: http://docs-im-beta.easemob.com/product/introduction.html


收起阅读 »

程序员副业接单做私活避坑指南

这篇文章系统的分享了对接单做私活这件事情的思考,也给出一些干货建议。希望让大家少走一些弯路,不要被坑。 先说结论 再说原因1.这篇文章 93年程序员在北京买房后,又开始思考怎么多赚点钱了 有详细分享:我能在北京买房是因为我工作特别稳定,5年社保未中断,且一直...
继续阅读 »

这篇文章系统的分享了对接单做私活这件事情的思考,也给出一些干货建议。希望让大家少走一些弯路,不要被坑。


先说结论



再说原因

1.这篇文章 93年程序员在北京买房后,又开始思考怎么多赚点钱了 有详细分享:我能在北京买房是因为我工作特别稳定,5年社保未中断,且一直快速的升职加薪。接私活对于赚钱买房只是锦上添花的作用,并不是买房的关键。

2.这篇文章 就业环境不好想搞副业?万字泣血解析割韭菜内幕! 详细有讲:作为程序员或者大学生,你想接单赚钱可能会遇到的坑。 或者最简单的一句话:让你先交钱的都是坑。 常见的包括但不限于:入驻平台收费、各种高大上的承诺。 再补充一句更简单的真理:凡是让你感觉占便宜了,天上掉馅饼的好事都是陷阱。

3.这篇文章 程序员全职接单一个月的感触 详细有讲:不要全职接单!不要全职接单!不要全职接单!


看到这里的老粉丝可能奇怪了,那阳哥是咋接单的呢?


我咋接单?


没错,我确实是有接单的,并且比较稳,也赚了一些钱。而且这些钱赚的踏踏实实,不急不躁。


为什么?


原因很简单:因为我比较靠谱,我接的单子也比较靠谱。



1. 接单来源


首先,我并没有从任何接单平台接过私活。都是朋友找我帮忙,他们觉得我能做,并且希望我做。


技巧:第一单不赚钱,赚个口碑,帮个忙交个朋友。人情比钱有价值。


划重点:你做事靠谱,别人才会找你,才会持续的找你,才会有更多的机会。


2. 学会拒绝


虽然“人情”很重要,但是也要学会觉得,不靠谱的单子一定不要接!包括但不限于:涉h涉z、博彩赌博等等很刑的项目、或者你任何心里犯嘀咕的项目。


只要你心里犯嘀咕了,纠结了。请不要犹豫,拒绝,干脆的拒绝!不要做丢西瓜捡芝麻的傻事!


3. 如何排期


评估一下自己的时间,如果工作不忙,按时下班,可以排期紧凑一些。


如果自己本身就很忙,接单会影响工作。那就拒绝,或者做个顺水人情,推荐给靠谱的朋友做。


4. 如何报价


根据自己的工资,算一下时薪或者日薪。


根据 排期*时薪(日薪) 就是报价。请不要不好意思报价,更不用觉得自己报的高或者低。


你只管先去报价,如果对方真心找你,会和你商量报价。而不是直接说行或者不行。


如果你报价之后,什么都不和你说,没下文了。大概率不是你的问题,可能他就是白嫖你的报价做参考,建议远离。


5. 如何签合同


合同一定要签,明确双方责任和义务:明确排期、功能点、违约责任。关键就是我说的这三点,其他的可以套模板。需要模板的可以私信我要一份,以备不时之需。


6. 如何提高成单率

1.自己靠谱,打造好口碑

2.学会表达,展示自己的优势

3.及时沟通,不管成与不成,及时沟通。


如果大家感兴趣,可以关注我的视频号聊一聊:王中阳Go


诚恳建议


踏踏实实做好本职工作,提升自己,在有能力之后,自然能够“清风徐来”。不需要你找项目,项目会主动找你的。


接单平台


下文是接单平台,内容来自知乎,转载过来的原因有2个:

1.方便大家了解这些平台各自的优势,可以结合自己的情况,注册一两个实践一下。注意哦:请态度随缘,不要期望太高。 如果你去年被优化,目前还没有找到工作,建议踏踏实实去找工作,不要在这上面浪费时间。

2.第二个原因也是想劝退大家入坑:这么多众包平台,接单平台。去看下注册率和成单率,很差的。而且好的项目基本都被头部的外包公司垄断了,凭啥一个刚入行的小菜鸟能接到单,换位思考一下,科学吗!?


一、垂直众包平台


这类平台是从 15 年到18年开始出现的,专注于 IT 众包领域,职位内容大多集中于 UI 设计、产品设计、程序开发、产品运营等需求,其中又以程序开发和 UI 设计的需求最多,可以提供比较稳定和比较多的兼职需求来供我们选择。这些渠道主要有:


1、YesPMP平台:


http://www.yespmp.com/


首推这个平台的原因只有一个:免费!注册免费,投标免费,而且资源不少。


但是每个平台都是有“套路的”,每天只能免费投递3个项目竞标,你如果想竞标更多的项目需要开会员。


(教你一招:第二天再投3个项目竞标不就行了,每天都可以免费投递三个)


2、开源众包 :


zb.oschina.net/projects/li…


开源中国旗下众包平台,目前项目以项目整包为主,对接企业接包方多些,个人也可以注册。目前有免费模式和付费模式。平台搞到最后都是为了赚钱,白嫖怪不太可能接到好项目。


3、人人开发 - 应用市场开发服务平台:


http://www.rrkf.com/


人人开发的注册流程比较简单一点,但是建议大家也要认真填写简历。


4、英选 :


http://www.yingxuan.io/


英选有自己的接包团队进行自营业务,也支持外部入驻。


5、我爱方案网:


http://www.52solution.com/


名字比较土,但是对于硬件工程师和嵌入式工程师建议注册下。


6、码市:


codemart.com/


7、解放号:


http://www.jfh.com/


二、线上技术论坛


1、GitHub


开发者最最最重要的网站:github.com


这个不用多说了吧,代码托管网站,上面有很多资源,想要什么轮子,上去搜就好了。


2. Stack Overflow


解决 bug 的社区:stackoverflow.com/


开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。


在这里能够与很多有经验的开发者交流,如果你是有经验的开发者,还可以来这儿帮助别人解决问题,提升个人影响力。


3. 程序员客栈:


http://www.proginn.com/


程序员客栈是领先的程序员自由工作平台,如果你是有经验有资质的开发者,都可以来上面注册成为开发者,业余的时候做点项目,赚点零花钱。


当然,如果你想成为一名自由工作者,程序员客栈也是可以满足的。只要你有技术,不怕赚不到钱。很多程序员日常在这里逛一下,接一点项目做。很多公司也在这发布项目需求。


4. 掘金


帮助开发者成长的技术社区:juejin.cn/


这个就不用我多说了吧:现在国内优质的开发者交流学习社区,可以去看大佬们写的文章,也可以自己分享学习心的,与更多开发者交流。认识更多的小伙伴儿,提升个人影响力。


5. v2ex


http://www.v2ex.com/


V2EX 是一个关于分享和探索的地方,上面有很多各大公司的员工,程序员。你想要的应有尽有。


6.电鸭社区


eleduck.com/


最近有朋友想找远程办公的岗位,电鸭社区值得好好看一看,可以说是国内远程办公做的相当好的社区了。


7. Medium


medium.com/


国外优质文章网站,Medium 的整体结构非常简单,容易让用户沉下心来专注于阅读。上面有很多高质量的技术文章,有很多厉害的人在上面发布内容。


8. Hacker News


news.ycombinator.com/news


国外优质文章网站,上面有很多高质量的技术文章,有很多厉害的人在上面分享内容。


9. GeeksforGeeks


http://www.geeksforgeeks.org/


GeeksforGeeks is a computer science portal for geeks。


10.飞援


http://www.freetalen.com/


是一个为程序员、产品经理、设计提供外包兼职和企业雇佣的兼职平台,致力于提供品质可控、体验卓越的专业技术人才灵活雇佣服务。


遥祝


遥祝大家在新的一年顺利上岸,找到心仪的工作,升职加薪。


在保证主业工作稳定之后,再搞副业,再去接单。


如果觉得本文对你有帮助,欢迎点个关注,不错过干货分享。


如果对接单搞副业实在感兴趣的话,可以关注私信我,后面有好项目分享给你。


最后再次友情提醒:还是踏踏实实上班吧!


作者:王中阳Go
链接:https://juejin.cn/post/7195085041456644154
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android爬坑日记四】组合替代继承,减少Base类滥用

背景 先说一下背景,当接触了比较多的项目之后,其实会发现每一个项目都会封装BaseActivity、BaseFragment等等。其实初衷其实是好的。每一个Activity和Fragment都是很多模板代码的,为了减少模板代码,封装进Base类其实是一种比较方...
继续阅读 »

背景


先说一下背景,当接触了比较多的项目之后,其实会发现每一个项目都会封装BaseActivity、BaseFragment等等。其实初衷其实是好的。每一个Activity和Fragment都是很多模板代码的,为了减少模板代码,封装进Base类其实是一种比较方便且可行的选择。


Base类涵盖了抽象、继承等面向对象特性,用得好会减少很多样板代码,但是一旦滥用,会对项目有很多弊端。


举个例子


当项目大了,需要封装进Base类的逻辑会非常多,比如说打印生命周期、ViewBinding 或者DataBinding封装、埋点、监听广播、监听EventBus、展示加载界面、弹Dialog等等其他业务逻辑,更有甚者把需要Context的函数都封装进Base类中。


以下举一个BaseActivity的例子,里面封装了上面所说的大部分情况,实际情况可能更多。


abstract class BaseActivity<T: ViewBinding, VM: ViewModel>: AppCompatActivity {

protected lateinit var viewBinding: T

protected lateinit var viewModel: VM

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 打印日志!!
ELog.debugLifeCycle("${this.localClassName} - onCreate")

// 初始化viewModel
viewModel = initViewModel()
// 初始化视图!!
initView()
// 初始化数据!!
initData()
// 注册广播监听!!
registerReceiver()
// 注册EventBus事件监听!!
registerEventBus()

// 省略一堆业务逻辑!

// 设置导航栏颜色!!
window.navigationBarColor = ContextCompat.getColor(this, R.color.primary_color)
}

protected fun initViewModel(): VM {
// 初始化viewModel
}

private fun initViewbinding() {
// 初始化viewBinding
}

// 让子类必须实现
abstract fun initView()

abstract fun initData()

private fun registerReceiver() {
// 注册广播监听
}

private fun unregisterReceiver() {
// 注销广播监听
}

private fun registerEventBus() {
// 注册EventBus事件监听
}

protected fun showDialog() {
// 需要用到Context,因此也封装进来了
}

override fun onResume() {
super.onResume()
ELog.debugLifeCycle("${this.localClassName} - onResume")
}

override fun onPause() {
super.onPause()
ELog.debugLifeCycle("${this.localClassName} - onPause")
}

override fun onDestroy() {
super.onDestroy()
ELog.debugLifeCycle("${this.localClassName} - onDestroy")
unregisterReceiver()
}
}

其实看起来还好,但是在使用的时候难免会遇到一些问题,对于中途接手项目的人来说问题更加明显。我们从中途接手项目的心路历程看看Base类的缺陷。


心路历程




  1. 当创建新的Activity或者Fragment的时候需要想想有没有逻辑可以复用,就去找Base类,或许写Base类的人不同,发现一个项目中可能会存在多个Base类,甚至Base类仍然有多个Base子类实现不同逻辑。这个时候就需要去查看分析每个Base类分别实现了什么功能,决定继承哪个。




  2. 如果一个项目中只有一个Base类的话,仍需要看看Base类实现了什么逻辑,没有实现什么逻辑,防止重复写样板代码。




  3. 当出现Base类实现了的,而自己本身并不想需要,例如不想监听广播或者不想用ViewModel,对于不想监听广播的情况就要特殊做适配,例如往Base类加标志位。对于不想用ViewModel但是由于泛型限制,还是只能传进去,不然没法继承。




  4. 当发现自己集成Base类出BUG了,就要考虑改子类还是改Base类,由于大量的类都集成了Base类,显然改Base类比较麻烦,于是改自己比较方便。




  5. 如果一个Activity中展示了多个Fragment,可能会有业务逻辑的重复,其实只需要一个就好了。




其实第一第二点还好,时间成本其实没有重复写样板代码那么高。但是第三点的话其实用标志位来决定Base类的功能哪个需要实现哪个不需要实现并不是一种优雅的方式,反而需要重写的东西多了几个。第四点归根到底就是Base类其实并不好维护。


爬坑


那么对于Base类怎样实践才比较优雅呢?在我看来组合替代继承其实是一种不错的思路。对于Kotlin first的Android项目来说,组合替代继承其实是比较容易的。以下仅代表个人想法,有不同意见可以交流一下。


成员变量委托


对于ViewModel、Handler、ViewBinding这些Base变量使用委托的方式是比较方便的。


对于ViewBinding委托可以看看我之前的文章,使用起来其实是非常简单的,只需要一行代码即可。


// Activity
private val binding by viewBinding(ActivityMainBinding::inflate)
// Fragment
private val binding by viewBinding(FragmentMainBinding::bind)

对于ViewModel委托,官方库则提供了一个viewBindings委托函数。


private val viewModel:HomeViewModel by viewModels()

需要在Gradle中引入ktx库


implementation 'androidx.fragment:fragment-ktx:1.5.1'
implementation 'androidx.activity:activity-ktx:1.5.1'

而对于Base变量则尽量少封装在Base类中,需要使用可以使用委托,因为如果实例了没有使用其实是比较浪费内存资源的,尽量按需实例。


扩展方法


对于需要用到Context上下文的逻辑封装到Base类中其实是没有必要的,在Kotlin还没有流行的时候,如果说需要使用到Context的工具方法,使用起来其实是不太优雅的。


例如展示一个Dialog:


class DialogUtils {
public static void showDialog(Activity activity, String title, String content) {
// 逻辑
}
}

使用起来就是这样:


class MyActivity : AppCompatActivity() {
...
fun initButton() {
button.setOnClickListener {
DialogUtils.showDialog(this, "title", "content")
}
}
}

使用起来可能就会有一些迷惑,第一个参数把自己传进去了,这对于展示Dialog的语义上是有些奇怪的。按理来说只需要传title和content就好了。


这个时候就会有人想着把这个封装到Base类中。


public abstract class BaseActivity extends AppCompatActivity {

protected void showDialog(String title, String content) {
// 这里就可以用Context了
}
}


使用起来就是这样:


class MyActivity : AppCompatActivity() {
...
fun initButton() {
button.setOnClickListener {
showDialog("title", "content")
}
}
}


是不是感觉好很多了。但是写在Base类中在Java中比较好用,对于Kotlin则完全可以使用扩展函数语法糖来替代了,在使用的时候和定义在Base类是一样的。


fun Activity.showDialog(title: String, content: String) {
// this就能获取到Activity实例
}

class MyActivity : AppCompatActivity() {
...
fun initButton() {
button.setOnClickListener {
// 使用起来和定义在Base类其实是一样的
showDialog("title", "content")
}
}
}


这也说明了,需要使用到Context上下文的函数其实不用在Base类中定义,直接定义在顶层就好了,可以减少Base类的逻辑。


注册监听器


对于注册监听器这种情况则需要分情况,监听器是需要根据生命周期来注册和取消注册的,防止内存泄漏。对于不是每个子类都需要的情况,有的人可能觉得提供一个标志位就好了,如果不需要的话让子类重写。如果定义成抽象方法则每个子类都要重写,如果不是抽象方法的话,子类可能就会忘记重写。在我看来获取生命周期其实是比较简单的事情。按需添加代码监听就好了。


那么什么情况需要封装在Base类中呢?




  • 怕之后接手项目的人忘记写这部分代码,则可以写到Base类中,例如打印日志或者埋点。




  • 而对于界面太多难以测试的功能,例如收到被服务器踢下线的消息跳到登录页面,这个可以写进Base类中,因为基本上每个类都需要监听这种消息。




总结


没有最优秀的架构,只有最适合的架构!对于Base类大家的看法都不一样,追求更少的工作量完成更多事情这个目的是统一的。而Base类一旦臃肿起来了会造成整个项目难以维护,因此对于Base类应该辩证看待,养成只有必要的逻辑才写在Base类中的习惯,feature类应该使用组合的方式来使用,这对于项目的可维护性和代码的可调试性是有好处的。


作者:米奇律师
链接:https://juejin.cn/post/7144671989159067656
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

关于缓存,每个开发人员都应该知道的3个问题

前言 虽然缓存被认为是软件系统的性能助推器,但如果处理不当,它也容易出现错误。 在本文中,我将介绍 3 个有时可能会造成灾难性后果的常见缓存问题,希望大家在架构上引入缓存时,需要考虑到。 缓存击穿 缓存故障 当缓存键过期时会发生缓存故障,并且多个请求同时访问...
继续阅读 »

前言


虽然缓存被认为是软件系统的性能助推器,但如果处理不当,它也容易出现错误。


在本文中,我将介绍 3 个有时可能会造成灾难性后果的常见缓存问题,希望大家在架构上引入缓存时,需要考虑到。


缓存击穿



缓存故障


当缓存键过期时会发生缓存故障,并且多个请求同时访问数据库以查找相同的键。


让我们来看看它是如何工作的:



  • 热缓存键过期。

  • 多个并发请求进来搜索同一个键。

  • 服务器向数据库发起多个并发请求以查找相同的键。


缓存击穿会显著增加数据库的负载,尤其是当许多热键同时过期时。


下面是解决这个问题的2种解决方案:



  • 获取搜索到的key的分布式锁,当一个线程试图更新缓存时,其他线程需要等待。

  • 利用Refresh-ahead 策略异步刷新热数据,使热键永不过期。


缓存穿透



缓存穿透


当搜索到的key既不在缓存中, 也不在数据库中时,就会发生缓存穿透, 连数据库都穿透过去了。


让我们来看看它是如何工作的,当key既不在缓存中也不在数据库中时,就会发生这种情况。



  • 当用户查询key时,应用程序由于缓存未命中而去查询数据库数据库。

  • 由于数据库不包含该key并返回空结果,因此该key也不会被缓存。

  • 因此,每个查询最终都会导致缓存未命中,而命中数据库,直接进行查库。


虽然乍一看这似乎微不足道,但攻击者可以通过使用此类密钥启动大量搜索来尝试破坏你的数据库


为了解决这个问题,我们可以:



  • 缓存过期时间较短的空结果。

  • 使用布隆过滤器。在查询数据库之前,应用程序在布隆过滤器中查找key,如果key不存在则立即返回。


缓存雪崩



当对数据库的请求突然激增时,就会发生缓存雪崩


这发生在:



  • 许多缓存数据同时过期。

  • 缓存服务宕机,所有请求都直接访问数据库。


数据库流量的突然激增可能会导致级联效应,并可能最终导致您的服务崩溃。


下面是一些常见的解决方案:



  • 调整缓存键的过期时间,使它们不会同时过期。

  • 使用Refresh-ahead 策略异步刷新热数据,使其永不过期。

  • 使用缓存集群来避免单点故障。当主节点崩溃时,其中一个副本被提升为新的主节点。


总结


虽然这些缓存问题起初看起来微不足道,但有时可能会对我们的下游客户端和依赖项产生级联效应。事先了解它们可以让我们设计一个更强大的系统,也可以简化我们的故障排除过程。


作者:JAVA旭阳
链接:https://juejin.cn/post/7197616340785774650
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

浅谈TheadLocal的使用场景和注意事项

概念 ThreadLocal 是Java的一个类,是一个本地线程,提供了一种线程安全的方式,主要用来避免共享数据(线程变量隔离)。 有时候可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例;就是说,每个线程都有一个伴生的空间(Th...
继续阅读 »

概念



ThreadLocalJava的一个类,是一个本地线程,提供了一种线程安全的方式,主要用来避免共享数据(线程变量隔离)。



有时候可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例;就是说,每个线程都有一个伴生的空间(ThreadLocal),存储私有的数据,只要线程在,就能拿到对应线程的ThreadLocal中存储的值。


TheadLocal的使用场景和注意事项


ThreadLocalJava开发中非常常见,一般在以下情况会使用到ThreadLocal



  • 在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次间的约束。

  • 线程间数据隔离,比如:上下文ActionContext、ApplicationContext

  • 进行事务处理,用于存储线程事务信息。


image.png


在使用ThreadLocal的时候,最常用的方法就是:initialValue()、set(T t)、get()、remove()


image.png


创建以及提供的方法


创建一个线程局部变量,其初始值通过调用给定的提供者(Supplier)生成;


public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

// InitialValue()初始化方式使用Java 8提供的Supplier函数接口会更加简介
ThreadLocal<String> userContext = ThreadLocal.withInitial(String::new);

这里就列出用的比较多的方法:


将此线程局部变量的当前线程副本设置为指定值;value表示要存储在此线程本地的当前线程副本中的值


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

返回此线程局部变量的当前线程副本中的值。 如果该变量对于当前线程没有值,则首先将其初始化为调用initialValue方法返回的值


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

删除此线程局部变量的当前线程值


public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

项目实例


以下是个人使用的场景:


为什么会使用它,如果在项目中想直接获取当前登录用户的信息,这个功能就可以使用ThreadLocal实现。


/**
* 登录用户信息上下文
*
* @author: austin
* @since: 2023/2/8 13:47
*/
public class UserContext {

private static final ThreadLocal<User> USER_CONTEXT = ThreadLocal.withInitial(User::new);

public static void set(User user) {
if (user != null) {
USER_CONTEXT.set(user);
}
}

public static User get() {
return USER_CONTEXT.get();
}

public static void remove() {
USER_CONTEXT.remove();
}

public static User getAndThrow() {
User user = USER_CONTEXT.get();
if (user == null || StringUtils.isEmpty(user.getId())) {
throw new ValidationException("user info not found!");
}
return user;
}
}

上面其实是定义了一个用户信息上下文类,关于上下文(context),我们在开发的过程中经常会遇到,比如SpringApplicationContext,上下文是贯穿整个系统或者阶段生命周期的对象,其中包含一些全局的信息,比如:登录后用户信息、账号信息、地址区域信息以及在程序的每一个阶段运行时的数据。


👏有了这个用户上下文对象之后,接下来就可以在项目中使用:


在该项目中个人使用的地方在登录拦截器中,当对登录的信息检查成功后,那么将当前的用户对象加入到ThreadLocal中:


User currentUser = userService.login(token.getUsername(), String.valueOf(token.getPassword()));
// 用户登录认证成功,UserContext存储用户信息
UserContext.put(currentUser);

Serivce实现层使用的时候,直接调用ThreadLocal中的get方法,就可以获得当前登录用户的信息:


//获取当前在线用户信息
User user = UserContext.get();

资源调用完成后需要在拦截器中删除ThreadLocal资源,防止内存泄漏问题:


@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//使用完的用户信息需要删除,防止内存泄露
UserContext.remove();
}

ThreadLocal的内存泄露问题🤢


如果我们在使用完该线程后不进行ThreadLocal中的变量进行删除,那么就会造成内存泄漏的问题,那么该问题是怎么出现的?


首先先分析一下ThreadLocal的内部结构:


ThreadLocal内部结构.png


先明确一个概念:对应在栈中保存的是对象的引用,对象的值是存储在堆中,如上图所示:其中Heap中的mapThreadLocalMap, 里面包含keyvalue, 其中value就是我们需要保存的变量数据,key则是ThreadLocal实例,上述图片的连接有实线和虚线,实线代表强引用,虚线表示弱引用。



即:每一个Thread维护一个ThreadLocalMap, key为使用 弱引用ThreadLocal实例,value为线程变量的副本。



扫盲强引用、软引用、弱引用、虚引用:😂


不同的引用类型呢,主要体现在对象的不同的可达性状态和对垃圾收集的影响:


强引用Java最常见的一种引用,只要还有强引用指向一个对象,那么证明该对象一定还活着,一定为可达性状态,不会被垃圾回收机制回收,因此,强引用是造成Java内存泄漏的主要原因。


软引用 是通过SoftReference实现的,如果一个对象只要软引用,那么在系统内存空间不足的时候会试图回收该引用指向的对象。


弱引用 是通过WeakReference实现的,如何一个对象只有弱引用,在垃圾回收线程扫描它所管辖的内存区域的时候,一旦发现只有弱引用指向的对象时候,不管当前的内存空间是否足够,垃圾回收器都会去回收这样的一个内存。


虚引用 形同虚设的东西,在任何情况下都可能被回收。


我们都知道,map中的value需要key找到,key没了,那么value就会永远的留在内存中,直到内存满了,导致OOM,所以我们就需要使用完以后进行手动删除,这样能保证不会出现因为GC的原因造成的OOM问题;当ThreadLocal Ref显示的指定为null时,关系链就变成了下面所示的情况:


ThreadLocal内存泄漏.png


ThreadLocal被显示显的指定为null之后,JVM执行GC操作,此时堆内存中的Thread-Local被回收,同时ThreadLocalMap中的Entry.key也成为了null,但是value将不会被释放,除非当前线程已经结束了生命周期的Thread引用被垃圾回收器回收。


ThreadLocal解决SimpleDateFormat非线程安全问题


为了找到问题所在,我们尝试查看SimpleDateFormatformat方法的源码来排查一下问题,format方法源码如下:


private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {

// 注意到此行setTime()方法代码
calendar.setTime(date);

boolean useDateFormatSymbols = useDateFormatSymbols();

for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}

switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;

case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;

default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

从上述源码可以看出,在执行SimpleDateFormat.format()方法时,会使用calendar.setTime()方法将输入的时间进行转换,那么我们想象一下这样的场景:



  • 线程 1 执行了calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

  • 线程 1 暂停执行,线程 2 得到CPU时间片开始执行;

  • 线程 2 执行了calendar.setTime(date)方法,对时间进行了修改;

  • 线程 2 暂停执行,线程 1 得出CPU时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。


正常情况下,程序执行是这样的:


image.png


非线程安全的执行流程是这样的:


image.png


了解了ThreadLocal的使用之后,我们回到本文的主题,接下来我们将使用ThreadLocal来实现100个时间的格式化,具体实现代码如下:


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 多线程时间工具类:ConcurrentDateUtil
*
* @author: austin
* @since: 2023/2/8 15:36
*/
public class ConcurrentDateUtil {

private static final String date_format = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

public static DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(date_format);
threadLocal.set(df);
}
return df;
}

public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}

public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}
}

当然也可以使用:



  • Apache commons包的DateFormatUtils或者FastDateFormat实现,宣称是既快又线程安全的SimpleDateFormat,并且更高效。

  • 使用Joda-Time类库来处理时间相关问题。


总结


本文简单的介绍了ThreadLocal的应用场景,其主要用在需要每个线程独占的元素上,例如SimpleDateFormat。然后,就是介绍了ThreadLocal的实现原理,详细介绍了set()get()方法,介绍了ThreadeLocalMap的数据结构,最后就是说到了ThreadLocal的内存泄露以及避免的方式。


作者:austin流川枫
链接:https://juejin.cn/post/7197673814179070010
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter & ChatGPT | 代码生成器

ChatGPT 作为一个自然语言处理工具,已经火了一段时间。对待 ChatGPT 不同人有着不同的看法,新事物的出现必然如此。利益相关者形成 抵制 和 狂热 两极;哗众取宠者蹭蹭热度,问些花活,博人眼球;猎奇者尝尝鲜,起哄者挑挑火;实用派在思考新事物的价值和...
继续阅读 »

ChatGPT 作为一个自然语言处理工具,已经火了一段时间。对待 ChatGPT 不同人有着不同的看法,新事物的出现必然如此。利益相关者形成 抵制狂热 两极;哗众取宠者蹭蹭热度,问些花活,博人眼球;猎奇者尝尝鲜,起哄者挑挑火;实用派在思考新事物的价值和劳动力:


image.png


对于那些拿 ChatGPT 当百科全书来用的,或询问哲学问题的朋友,我只想说:



对于一个问题,用错误的工具去处理得出错误的结果,是一件很正常的事。





1. ChatGPT 的特点和劣势

ChatGPT 最大的特点是基础的语义分析,让计算机对自然语言进行处理并输出。在一段会话中,上下文是有效的,所以可以类似于交流。



问这个问题,它会怎么回答?



这种猎奇的心理,会让一部分人期望尝试;有稀奇古怪或愚蠢的回答,也可以满足人类对人工智障的优越感;分享问答,也让 ChatGPT 拥有一丝的社交属性。蹭热度、猎奇、起哄三者可以用它填充一块内心的空虚,也仅止步于此。




ChatGPT 目前的劣势也很明显,由于数据是几年前的,所以时效性不强;对很多问题回答的精准度并不高,对于盲目相信的人,或判别力较差的朋友并不友好;最后一点,非常重要:对于工具而言,如果对其依赖性太高,脱离工具时,会让人的主观能动性降低。


image.png




2. 代码的生成与规则诱导

如下所示,让它生成一个 Dart 的 User 类:



生成一个 dart 类 User, 字段为 : 可空 int 型 age 、final 非空 String 型 username 默认值为 “unknown”



image.png


虽然代码给出了,但是可以看出,这是空安全之前的代码。可能很多人到这里,觉得数据陈旧没什么用途,就拜拜了您嘞。


image.png




但它是一个有会话上下文的自然语言处理工具,你可以让它理解一些概念。就像一个新员工,上班第一天出了一点小错误,你是立刻开除他,还是告诉他该怎么正确处理。如下所示,给了它一个概念:



Dart 新版本中可空类型定义时,其后需要加 ?



image.png




如下所示,你就可以在当前的会话环境中让它生成更多字段的类型:



用 Dart 新版本生成一个 dart 类 User,字段为: final 非空 int 型 age , final 非空 String 型 username 默认值为 “unknown” , final 非空 int 型 height,可空 String型info,final 非空 int 型 roleId



image.png


如果存在问题,可以继续进行指正。比如 :



用 Dart 新版本,有默认值的字段不需要使用 required 关键字,其他非空字段需要



image.png


所以对于 ChatGPT 而言,我们可以把它看成一个有一些基础知识的,可为我们免费服务的员工,简称:奴隶。当它做错事时,你骂它,责备它,抛弃它是毫无意义的,因为它是机器。我们需要去 诱导 它理解,在当前工作环境中正确的事。


这样在当前会话中,它就可以理解你诉说的规则,当用它创建其他类时,他就不会再犯错。并且不排除它会基于你的规则,去完善自身的 知识储备 ,当众多的人用正确的规则去 诱导 它,这就是一个善意的正反馈。




3. 解决方案的概念

这里从生成的代码 不支持空安全支持空安全,其实只用了几句话。第一句是反馈测试,看看它的 默认知识储备



生成一个 dart 类 User, 字段为 : 可空 int 型 age 、final 非空 String 型 username 默认值为 “unknown”



当它的输出不满足我们的需求时,再进行 诱导



Dart 新版本中可空类型定义时,其后需要加 ?

用 Dart 新版本,有默认值的字段不需要使用 required 关键字,其他非空字段需要



在诱导完成之后,它就可以给出满足需求的输出。这种诱导后提供的会话环境,输出是相对稳定的,完成特定的任务。这就是为不确定的输出,添加规则,使其输出趋近 幂等性 。一旦一项可以处理任务的工具有这种性质,就可以面向任何人使用。可以称这种诱导过程为解决某一问题的一种 解决方案


比如上面的三句话就是:根据类信息生成 Dart 数据类型,并支持空安全。在当前环境下,就可以基于这种方案去处理同类的任务:



用 Dart 新版本生成一个 dart 类 TaskResult,字段为: final 非空 int 型 cost , final 非空 String 型 taskName 默认值为 “unknown” , final 非空 int 型 count,可空 String型taskInfo,final 非空 String型 taskCode



image.png


你拷贝代码后,就是可用的:


image.png




4. Dart 数据类生成器完善

上面生成 Dart 数据类比较简单,下面继续拓展,比如对于数据类型而言 copyWithtoJsonfromJson 的方法自己写起来比较麻烦。如果现在告诉它:



为上面的类提供 copyWith、toJson 、 fromJson 方法



它会进行提供,说明它具有这个 默认知识储备 ,但可以看到 copyWith 方法中的字段不符合空安全:


image.png


此时可以训练它的 类型可空 的意识,让它主动处理类似的问题,也可以直白的告诉它



将上面的 copyWith 方法入参类型后加 ? 号



这样生成的 TaskResult 类就可以使用了:


image.png


class TaskResult {
final int cost;
final String taskName;
final int count;
final String? taskInfo;
final String taskCode;

TaskResult({
required this.cost,
this.taskName = 'unknown',
required this.count,
this.taskInfo,
required this.taskCode,
});

TaskResult copyWith({
int? cost,
String? taskName,
int? count,
String? taskInfo,
String? taskCode,
}) {
return TaskResult(
cost: cost ?? this.cost,
taskName: taskName ?? this.taskName,
count: count ?? this.count,
taskInfo: taskInfo ?? this.taskInfo,
taskCode: taskCode ?? this.taskCode,
);
}

Map toJson() {
return {
'cost': cost,
'taskName': taskName,
'count': count,
'taskInfo': taskInfo,
'taskCode': taskCode,
};
}

static TaskResult fromJson(Map json) {
return TaskResult(
cost: json['cost'] as int,
taskName: json['taskName'] as String,
count: json['count'] as int,
taskInfo: json['taskInfo'] as String,
taskCode: json['taskCode'] as String,
);
}
}



5. 代码生成字符串 与 ChatGPT 生成字符串

对于一些相对固定的代码,可以使用代码逻辑,拼接字符串来生成。如下所示,通过对类结构的抽象化,使用对象进行配置,输出字符串。我们来思考一下,这和 ChatGPT 生成代码的区别。


首先,使用代码生成代码是一种完全的 幂等行为 。也就是说任何人、在任何时间、任何空间下,使用相同的输入,都可以获取到相同的输出,是绝对精准的。其产生代码的行为逻辑是完全可控的,人的内心是期待确定性的。


image.png


而 ChatGPT 对自然语言的理解,你可以用语言去引导它输出一些你的需求,比如 :



以json 格式生成 10 句连续的中文对话,key 为 content包括。另外 time 字段为时间戳 ,type 字段1,2 随机



image.png


其实没有什么孰强孰弱,只是使用场景的不同而已。刀在不同人的手里有不同的用法,人是生产生活的主体,工具只有服务的属性。驾驭工具,让它产生实用的价值,才是工具存在的意义。好了,本文到这里就扯完了,感谢观看 ~


作者:张风捷特烈
链接:https://juejin.cn/post/7197584339213762619
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

动态适配 web 终端的尺寸

web
使Xterminal组件自适应容器 通过 xtermjs 所创建的终端大小是由cols、rows这两个配置项的来决定,虽然你可以通过 CSS 样式来让其产生自适应效果,但是这种情况下字体会变得模糊变形等、会出现一系列的问题,要解决这个问题我们还是需要使用col...
继续阅读 »

使Xterminal组件自适应容器


通过 xtermjs 所创建的终端大小是由cols、rows这两个配置项的来决定,虽然你可以通过 CSS 样式来让其产生自适应效果,但是这种情况下字体会变得模糊变形等、会出现一系列的问题,要解决这个问题我们还是需要使用cols、rows这两个值来动态设置。


image.png


红色部分则是通过colsrows属性控制,我们可以很明显的看到该终端组件并没有继承父元素的宽度以及高度,而实际的宽度则是通过colsrows两个属性控制的。


如何动态设置cols和rows这两个参数。


我们去看官方官方文档的时候,会注意到,官方有提供几个插件供我们使用。


image.png


xterm-addon-fit: 可以帮助我们来让 web 终端实现宽度自适应容器。目前的话行数还不行,暂时没有找到好的替代方案,需要动态的计算出来,关于如何计算可以参数 vscode 官方的实现方案。


image.png


引入xterm-addon-fit,在我们的案例中,加入下面这两行:


image.png


动态计算行数


想要动态计算出行数的话,就需要获取到一个dom元素的高度:


image.png


动态计算尺寸的方法。


const terminalReact: null | HTMLDivElement = terminalRef.current // 100% * 100%
const xtermHelperReact: DOMRect | undefined = terminalReact?.querySelector(".xterm-helper-textarea")!.getBoundingClientRect()
const parentTerminalRect = terminalReact?.getBoundingClientRect()
const rows = Math.floor((parentTerminalRect ? parentTerminalRect.height : 20) / (xtermHelperReact ? xtermHelperReact.height : 1))
const cols = Math.floor((parentTerminalRect ? parentTerminalRect.width : 20) / (xtermHelperReact ? xtermHelperReact.width : 1))
// 调用resize方法,重新设置大小
termRef.current.resize(cols, rows)
复制代码

我们可以考虑封装成一个函数,只要父亲组件的大小发生变化,就动态适配一次。


作者:可视化高级双料技工
链接:https://juejin.cn/post/7160332506015727629
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »