注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

安卓开发基础——弱引用的使用

前言起因今天开发遇到一个问题,就是在快速点击带点击事件的控件,如果控件里面写的是Dialog弹窗就有概率出现弹窗连续在界面上出现两次,也就是你关闭弹窗后发现还有一个一样的弹窗在界面,这样就会带来不好的体验。结果2月9日在网上查了许多解决方法,就有提到将该Dia...
继续阅读 »

前言

起因

今天开发遇到一个问题,就是在快速点击带点击事件的控件,如果控件里面写的是Dialog弹窗就有概率出现弹窗连续在界面上出现两次,也就是你关闭弹窗后发现还有一个一样的弹窗在界面,这样就会带来不好的体验。

结果

2月9日

在网上查了许多解决方法,就有提到将该Dialog变成类的成员变量,不用每次都new就可能避免这种情况出现,但我着实不清楚为什么以及具体怎么做,于是请教了组里的大哥,大哥和我说他之前也处理过这种问题,使用了弱引用,可我还是不知道具体的实现方式,于是便找到大哥的代码,并在网上了解了弱引用的具体作用。

2月10日

今天我请教了我们掘金开发群的Java大佬,他告诉我,我这个写法仍然避免不了弹两次Dialog的,并给出意见,可以使用共享状态,推荐我创建一个共享的ReentrantLock,不过我还没去实现,等有时间再看看。

下面就让我们看看弱引用到底是什么。

正篇

弱引用的概念

想知道弱引用,那就得知道几个名词:

  • 强引用

  • 软引用

  • 弱引用

  • 虚引用

首先我们来看看这些词的概念:

  1. 强引用

强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  1. 软引用

软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。

  1. 弱引用

弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

  1. 虚引用

虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

以上定义都是参考自知乎回答 :强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么? - 知乎 (zhihu.com),从这我们可以了解到其实我们Java中new对象就是强引用,强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象,也就简而言之对象在引用时,不回收,上面说的文章中也举例说明了强引用的特点:


而我们本篇说的弱引用,则是发现即回收,它通常是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。

但是,又因为垃圾回收器的线程通常优先级很低,所以,一般并不一定能很快地发现持有弱引用的对象,而在这种情况下,弱引用对象就可以存在较长的时间。

而如何使用弱引用,我们接着往下看:

使用方法

前言提到我们使用了弱引用在开发中大哥已经使用过,所以我就跟着后面仿写一下就好,而知乎的那篇文章也提到:


这就基本是弱引用的定义方法,因为之前前言说的Dialog问题弱引用并没有真正起效果,所以我们换一种方法去展示他在安卓上的使用,那就是在使用Bitmap时防止OOM,写法如下:

ImageView imageView = findViewById(R.id.vImage);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher_background);
Drawable drawable = new BitmapDrawable(getResources(), bitmap);
WeakReference<Drawable> weakDrawable = new WeakReference<>(drawable);
Drawable bgDrawable = weakDrawable.get();
if(bgDrawable != null) {
   imageView.setBackground(drawable);
}

我们再对比一下普通的强引用方法:

ImageView imageView = findViewById(R.id.vImage);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher_background);
Drawable drawable = new BitmapDrawable(getResources(), bitmap);
imageView.setBackground(drawable);

其实,就是对drawable对象从强引用转为弱引用,这样一旦出现内存不足,不会直接去使用drawable对象,让JVM自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

总结

其实这块内容需要对GC机制很熟悉,我不是很熟,所以使用可能也出现不对,希望读者可以积极指正,谢谢观看!

作者:ObliviateOnline
来源:juejin.cn/post/7198519499867815997

收起阅读 »

Flutter Android多窗口方案落地(下)

接:Flutter Android多窗口方案落地(上)插件层封装。插件层就很简单了,创建好MethodCallHandler之后,直接持有单例的EngineManager就可以了。class FlutterMultiWindowsPlugin : Flutte...
继续阅读 »

接:Flutter Android多窗口方案落地(上)

  1. 插件层封装。插件层就很简单了,创建好MethodCallHandler之后,直接持有单例的EngineManager就可以了。

class FlutterMultiWindowsPlugin : FlutterPlugin, MethodCallHandler {
  companion object {
      private const val TAG = "MultiWindowsPlugin"
  }


   @SuppressLint("LongLogTag")
   override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
       Log.i(TAG, "onMessage: onAttachedToEngine")
       Log.i(TAG, "onAttachedToEngine: ${Thread.currentThread().name}")
       MessageHandle.init(flutterPluginBinding.applicationContext)

       MethodChannel(
           flutterPluginBinding.binaryMessenger,
           "flutter_multi_windows.messageChannel",
       ).setMethodCallHandler(this)
   }

   override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
       Log.i(TAG, "onDetachedFromEngine: ${Thread.currentThread().name}")
   }

   override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
       Log.i(TAG, "onMethodCall: thread : ${Thread.currentThread().name}")
       MessageHandle.onMessage(call, result)
   }
}
@SuppressLint("StaticFieldLeak")
internal object MessageHandle {
  private const val TAG = "MessageHandle"

   private var context: Context? = null
   private var manager: EngineManager? = null

   fun init(context: Context) {
       this.context = context
       if (manager != null)
           return
       // 必须单例调用
       manager = EngineManager.getInstance(this.context!!)
   }

   // 处理消息,所有管道通用。需要共享Flutter Activity
   fun onMessage(
       call: MethodCall, result: MethodChannel.Result
   ) {
       val params = call.arguments as Map<*, *>
       when (call.method) {
           "open" -> {
               Log.i(TAG, "onMessage: open")
               val map: HashMap<String, Any> = HashMap()
               map["needShowWindow"] = true
               map["name"] = params["name"] as String
               map["entryPoint"] = params["entryPoint"] as String
               map["width"] = (params["width"] as Double).toInt()
               map["height"] = (params["height"] as Double).toInt()
               map["gravityX"] = params["gravityX"] as Int
               map["gravityY"] = params["gravityY"] as Int
               map["paddingX"] = params["paddingX"] as Double
               map["paddingY"] = params["paddingY"] as Double
               map["draggable"] = params["draggable"] as Boolean
               map["type"] = params["type"] as String

               if (params["params"] != null) {
                   map["params"] = params["params"] as ArrayList<String>
               }
               result.success(manager?.showWindow(map, object : EngineCallback {
                   override fun onEngineDestroy(id: String) {
                   }
               }))
           }
           "close" -> {
               val windowId = params["windowId"] as String
               manager?.dismissWindow(windowId)
           }
           "executeTask" -> {
               Log.i(TAG, "onMessage: executeTask")
               val map: HashMap<String, Any> = HashMap()
               map["name"] = params["name"] as String
               map["entryPoint"] = params["entryPoint"] as String
               map["type"] = params["type"] as String
               result.success(manager?.executeTask(map))
           }
           "finishTask" -> {
               manager?.finishTask(params["taskId"] as String)
           }
           "setPosition" -> {
               val res = manager?.setPosition(
                   params["windowId"] as String,
                   params["x"] as Int,
                   params["y"] as Int
               )
               result.success(res)
           }
           "setAlpha" -> {
               val res = manager?.setAlpha(
                   params["windowId"] as String,
                   (params["alpha"] as Double).toFloat(),
               )
               result.success(res)
           }
           "resize" -> {
               val res = manager?.resetWindowSize(
                   params["windowId"] as String,
                   params["width"] as Int,
                   params["height"] as Int
               )
               result.success(res)
           }
           else -> {

           }
       }
   }
}

同时需要清楚,Engine通过传入的entryPoint,就可以找到Flutter层中的方法入口点,在入口点中runApp即可。

实现过程中的坑

在实现过程中我们遇到的值得分享的坑,就是Flutter GestureDetector和Window滑动事件的冲突。 由于悬浮窗是需要可滑动的,因此在原生层需要监听对应的事件;而Flutter的事件,是Android层分发给FlutterView的,两者形成冲突,导致Flutter内部滑动的时候,原生层也会捕获到,最终造成冲突。
如何解决?
从需求上来看,悬浮窗是否需要滑动,应该交给调用方决定,也就是由Flutter层来决定是否Android是否要对Flutter的滑动事件进行监听,即flutterView.setOnTouchListener。这里我们使用一种更轻量级的操作,FlutterView的监听默认加上,然后在事件处理中,我们通过变量来做处理;而Flutter通过MethodChannel改变这个变量,加快了通信速度,避免了事件来回监听和销毁。

flutterView.setOnTouchListener { _event ->
   when (event.action) {
       MotionEvent.ACTION_MOVE -> {
           if (dragging) {
               setPosition(
                   initialX + (event.rawX - startX).roundToInt(),
                   initialY + (event.rawY - startY).roundToInt()
              )
          }
      }
       MotionEvent.ACTION_UP -> {
           dragEnd()
      }
       MotionEvent.ACTION_DOWN -> {
           startX = event.rawX
           startY = event.rawY
           initialX = layoutParams.x
           initialY = layoutParams.y
           dragStart()
           windowManager.updateViewLayout(rootViewlayoutParams)
      }
  }
   false
}

dragging则是通过Flutter层去驱动的:FlutterMultiWindowsPlugin().dragStart();

private fun dragStart() {
   dragging = true
}

private fun dragEnd() {
   dragging = false
}

使用方式

目前我们内部已在4个应用落地了这个方案。应用方式有两种:一种是Flutter通过插件调用,也可以直接通过后台Service打开。效果尚佳,目的都是为了让Flutter的UI跨端使用。
另外,Flutter的方法入口点必须声明@pragma('vm:entry-point')

写在最后

目前来看这种方式可以完美支持Flutter在Android上开启多窗口,且能精准控制。但由于一个engine对应一个窗口,过多engine带来的内存隐患还是不可忽视的。我们希望Flutter官方能尽快的支持engine对应多个入口点,并且共享内存,只不过目前来看还是有点天方夜谭~~
这篇文章,需要有一定原生基础的同学才能看懂。只讲基础原理,代码不全,仅供参考! 另外多窗口的需求,不知道大家需求量如何,热度可以的话我再出个windows的多窗口实现!

作者:Karl_wei
来源:juejin.cn/post/7198824926722949179


收起阅读 »

Flutter Android多窗口方案落地(上)

前言Flutter在桌面端的多窗口需求,一直是个历史巨坑。随着Flutter的技术在我们windows、android桌面设备落地,我们发现多窗口需求必不可少,突破这个技术壁垒已经刻不容缓。实现原理1. 基本原理对于Android移动设备来说,多窗口的应用大多...
继续阅读 »

前言

Flutter在桌面端的多窗口需求,一直是个历史巨坑。随着Flutter的技术在我们windows、android桌面设备落地,我们发现多窗口需求必不可少,突破这个技术壁垒已经刻不容缓。

实现原理

1. 基本原理

对于Android移动设备来说,多窗口的应用大多是用于直播/音视频的悬浮弹窗,让用户离开应用后还能在小窗口中观看内容。实现原理是通过WindowManager创建和管理窗口,包括视图内容、拖拽、事件等操作。
我们都清楚Flutter只是一个可以做业务逻辑的UI框架,在Flutter中想要实现多窗口,也必须依赖Android的窗口管理机制。基于原生的Window,显示Flutter绘制的UI,从而实现跨平台的视图交互和业务逻辑。

2. 具体步骤

  • Android端基于Window Manager创建Window,管理窗口的生命周期和拖拽逻辑;

  • 使用FlutterEngineGroup来管理Flutter Engine,通过引擎吸附Flutter的UI,加入到原生的FlutterView;

  • 把FlutterView通过addView的方式加入到Window上。

3. 原理图


插件实现

基于上述原理,可以在Android的窗口显示Flutter的UI。但要真正提供给Flutter层使用,还需要再封装一个插件层。

  1. 通过单例管理多个窗口 由于是多窗口,可能项目中多个地方都会调用到,因此需要使用单例来统一管理所有窗口的生命周期,保证准确创建、及时销毁。

//引擎生命钩子回调,让调用方感知引擎状态
interface EngineCallback {
   fun onCreate(id:String)
   fun onEngineDestroy(idString)
}

class EngineManager private constructor(contextContext) {

   // 单例对象
   companion object :
       SingletonHolder<EngineManagerContext>(::EngineManager)

   // 窗口类型;如果是单一类型,那么同名窗口将返回上一次的未销毁的实例。
   private val TYPE_SINGLEString = "single"

   init {
       Log.d("EngineManager""EngineManager init")
  }

   data class Entry(
       val engineFlutterEngine,
       val windowAndroidWindow?
  )

   private var myContextContext = context

   private var engineGroupFlutterEngineGroup = FlutterEngineGroup(myContext)

   // 每个窗口对应一个引擎,基于引擎ID和名称存储多窗口的信息,以及查找
   private val engineMap = ConcurrentHashMap<StringEntry>() //搜索引擎,用作消息分发
   private val name2IdMap = ConcurrentHashMap<StringString>() //判断是否存在了任务
   private val id2NameMap = ConcurrentHashMap<StringString>() //根据任务获取name并清除
   private val engineCallback =
       ConcurrentHashMap<StringEngineCallback>() //通知调用方引擎状态 0-create 1-attach 2-destroy

   fun showWindow(
       paramsHashMap<StringAny>,
       engineStatusCallbackEngineCallback
  ): String? {
       val entryString?
       if (params.containsKey("entryPoint")) {
           entry = params["entryPoint"as String
      } else {
           return null
      }

       val nameString?
       if (params.containsKey("name")) {
           name = params["name"as String
      } else {
           return null
      }

       val type = params["type"]
       if (type == TYPE_SINGLE && name2IdMap[name!= null) {
           return name2IdMap[name]
      }

       val windowUid = UUID.randomUUID().toString()
       if (type == TYPE_SINGLE) {
           name2IdMap[name= windowUid
           id2NameMap[windowUid= name
           engineCallback[windowUid= engineStatusCallback
      }
       val dartEntrypoint = DartExecutor.DartEntrypoint(findAppBundlePath(), entry)
       val args = mutableListOf(windowUid)

       var userList<String>? = null
       if (params.containsKey("params")) {
           user = params["params"as List<String>
      }

       if (user != null) {
           args.addAll(user)
      }
       // 把调用方传递的参数回调给Flutter
       val option =
           FlutterEngineGroup.Options(myContext).setDartEntrypoint(dartEntrypoint)
              .setDartEntrypointArgs(
                   args
              )
       val engine = engineGroup.createAndRunEngine(option)
       val draggable = params["draggable"as Boolean? ?true
       val width = params["width"as Int? ?0
       val height = params["height"as Int? ?0

       val config = GravityConfig()
       config.paddingX = params["paddingX"as Double? ?0.0
       config.paddingY = params["paddingY"as Double? ?0.0
       config.gravityX = GravityForX.values()[params["gravityX"as Int? ?1]
       config.gravityY = GravityForY.values()[params["gravityY"as Int? ?1]
       // 把创建好的引擎传给AndroidWindow,由其去创建窗口
       val androidWindow =
           AndroidWindow(myContextdraggablewidthheightconfigengine)
       engineMap[windowUid= Entry(engineandroidWindow)
       androidWindow.open()
       engine.platformViewsController.attach(
           myContext,
           engine.renderer,
           engine.dartExecutor
      )
       return windowUid
  }

   fun setPosition(idString?xIntyInt): Boolean {
       id ?return false
       val entry = engineMap[id]
       entry ?return false
       entry.window?.setPosition(xy)
       return true
  }
   
   fun setSize(idString?widthdoubleheightdouble): Boolean {
       // ......
  }
}

通过代码我们可以看到,每个窗口都对应一个engine,通过name和生成的UUID做唯一标识,然后把engine传给AndroidWindow,在那里加入WindowManger,以及Flutter UI的获取。

  1. AndroidWindow的实现;通过context.getSystemService(Service.WINDOW_SERVICE) as WindowManager获取窗口管理器;同时创建FlutterView和LayoutInfalter,通过engine拿到视图吸附到FlutterView,把FlutterView加到Layout中,最后把Layout通过addView加到WindowManager中显示。

class AndroidWindow(
   private val contextContext,
   private val draggableBoolean,
   private val widthInt,
   private val heightInt,
   private val configGravityConfig,
   private val engineFlutterEngine
) {
   private var startX = 0f
   private var startY = 0f
   private var initialX = 0
   private var initialY = 0
   private var dragging = false
   private lateinit var flutterViewFlutterView
   private var windowManager = context.getSystemService(Service.WINDOW_SERVICEas WindowManager
   private val inflater =
       context.getSystemService(Service.LAYOUT_INFLATER_SERVICEas LayoutInflater
   private val metrics = DisplayMetrics()

   @SuppressLint("InflateParams")
   private var rootView = inflater.inflate(R.layout.floatingnullfalseas ViewGroup
   private val layoutParams = WindowManager.LayoutParams(
       dip2px(contextwidth.toFloat()),
       dip2px(contextheight.toFloat()),
       WindowManager.LayoutParams.TYPE_SYSTEM_ALERT// 系统应用才可使用此类型
       WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
       PixelFormat.TRANSLUCENT
  )

   fun open() {
       @Suppress("Deprecation")
       windowManager.defaultDisplay.getMetrics(metrics)
       layoutParams.gravity = Gravity.START or Gravity.TOP
       selectMeasurementMode()

       // 设置位置
       val screenWidth = metrics.widthPixels
       val screenHeight = metrics.heightPixels
       when (config.gravityX) {
           GravityForX.Left -> layoutParams.x = config.paddingX!!.toInt()
           GravityForX.Center -> layoutParams.x =
              ((screenWidth - layoutParams.width/ 2 + config.paddingX!!).toInt()
           GravityForX.Right -> layoutParams.x =
              (screenWidth - layoutParams.width - config.paddingX!!).toInt()
           null -> {}
      }

       when (config.gravityY) {
           GravityForY.Top -> layoutParams.y = config.paddingY!!.toInt()
           GravityForY.Center -> layoutParams.y =
              ((screenHeight - layoutParams.height/ 2 + config.paddingY!!).toInt()
           GravityForY.Bottom -> layoutParams.y =
              (screenHeight - layoutParams.height - config.paddingY!!).toInt()
           null -> {}
      }

       windowManager.addView(rootViewlayoutParams)
       flutterView = FlutterView(inflater.contextFlutterSurfaceView(inflater.contexttrue))
       flutterView.attachToFlutterEngine(engine)
       if (draggable) {
           @Suppress("ClickableViewAccessibility")
           flutterView.setOnTouchListener { _event ->
               when (event.action) {
                   MotionEvent.ACTION_MOVE -> {
                       if (dragging) {
                           setPosition(
                               initialX + (event.rawX - startX).roundToInt(),
                               initialY + (event.rawY - startY).roundToInt()
                          )
                      }
                  }
                   MotionEvent.ACTION_UP -> {
                       dragEnd()
                  }
                   MotionEvent.ACTION_DOWN -> {
                       startX = event.rawX
                       startY = event.rawY
                       initialX = layoutParams.x
                       initialY = layoutParams.y
                       dragStart()
                       windowManager.updateViewLayout(rootViewlayoutParams)
                  }
              }
               false
          }
      }
       @Suppress("ClickableViewAccessibility")
       rootView.setOnTouchListener { _event ->
           when (event.action) {
               MotionEvent.ACTION_DOWN -> {
                   layoutParams.flags =
                       layoutParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                   windowManager.updateViewLayout(rootViewlayoutParams)
                   true
              }
               else -> false
          }
      }

       engine.lifecycleChannel.appIsResumed()

       rootView.findViewById<FrameLayout>(R.id.floating_window)
          .addView(
               flutterView,
               ViewGroup.LayoutParams(
                   ViewGroup.LayoutParams.MATCH_PARENT,
                   ViewGroup.LayoutParams.MATCH_PARENT
              )
          )
       windowManager.updateViewLayout(rootViewlayoutParams)
  }
   // .....

续:Flutter Android多窗口方案落地(下)

作者:Karl_wei
来源:juejin.cn/post/7198824926722949179

收起阅读 »

AndroidQQ登录接入详细介绍

一、前言由于之前自己项目的账号系统不是非常完善,所以考虑接入QQ这个强大的第三方平台的接入,目前项目暂时使用QQ登录的接口进行前期的测试,这次从搭建到完善花了整整两天时间,不得不吐槽一下QQ互联的官方文档,从界面就可以看出了,好几年没维修了,示例代码也写的不是...
继续阅读 »

一、前言

由于之前自己项目的账号系统不是非常完善,所以考虑接入QQ这个强大的第三方平台的接入,目前项目暂时使用QQ登录的接口进行前期的测试,这次从搭建到完善花了整整两天时间,不得不吐槽一下QQ互联的官方文档,从界面就可以看出了,好几年没维修了,示例代码也写的不是很清楚,翻了好多源代码和官方的demo,这个demo可以作为辅助参考,官方文档的api失效了可以从里面找相应的替代,但它的代码也太多了,一个demo 一万行代码,心累,当时把demo弄到可以运行就花了不少时间,很多api好像是失效了,笔者自己做了一些处理和完善,几乎把sdk功能列表的登录相关的api都尝试了一下,真的相当的坑,正文即将开始,希望这篇文章能够给后来者一些参考和帮助。

二、环境配置

1.获取应用ID

这个比较简单,直接到QQ互联官网申请一个即可,官网地址

https://connect.qq.com

申请应用的时候需要注意应用名字不能出现违规词汇,否则可能申请不通过

应用信息的填写需要当前应用的包名和签名,这个腾讯这边提供了一个获取包名和签名的app供我们开发者使用,下载地址

https://pub.idqqimg.com/pc/misc/files/20180928/c982037b921543bb937c1cea6e88894f.apk

未通过审核只能使用调试的QQ号进行登录,通过就可以面向全部用户了,以下为审核通过的图片


2.官网下载相关的sdk

下载地址

https://tangram-1251316161.file.myqcloud.com/qqconnect/OpenSDK_V3.5.10/opensdk_3510_lite_2022-01-11.zip

推荐直接下载最新版本的,不过着实没看懂最新版本的更新公告,说是修复了retrofit冲突的问题,然后当时新建的项目没有用,结果报错,最后还是加上了,才可以


3. jar的引入

将jar放入lib包下,然后在app 同级的 build.gradle添加以下代码即完成jar的引用

dependencies {
...
   implementation fileTree(dir: 'libs', include: '*.jar')
  ...
}

4.配置Manifest

在AndroidManifest.xml中的application结点下增加以下的activity和启动QQ应用的声明,这两个activity无需我们在另外创建文件,引入的jar已经处理好了

 <application
      ...    
       <!--这里的权限为开启网络访问权限和获取网络状态的权限,必须开启,不然无法登录-->
       <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
       <activity
           android:name="com.tencent.tauth.AuthActivity"
           android:exported="true"
           android:launchMode="singleTask"
           android:noHistory="true">
           <intent-filter>
               <action android:name="android.intent.action.VIEW" />

               <category android:name="android.intent.category.DEFAULT" />
               <category android:name="android.intent.category.BROWSABLE" />

               <data android:scheme="tencent你的appId" />
           </intent-filter>
       </activity>
       <activity
           android:name="com.tencent.connect.common.AssistActivity"
           android:configChanges="orientation|keyboardHidden"
           android:screenOrientation="behind"
           android:theme="@android:style/Theme.Translucent.NoTitleBar" />

       <provider
           android:name="androidx.core.content.FileProvider"
           android:authorities="com.tencent.login.fileprovider"
           android:exported="false"
           android:grantUriPermissions="true">
           <meta-data
               android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
       </provider>
...
   </application>

上面的哪个代码的最后提供了一个provider用于访问 QQ 应用的,需要另外创建一个 xml 文件,其中的 authorities 是自定义的名字,确保唯一即可,这边最下面那个provider是翻demo找的,文档没有写,在res文件夹中新增一个包xml,里面添加文件名为file_paths的 xml ,其内容如下

<?xml version="1.0" encoding="utf-8"?>
<paths>
   <external-files-path name="opensdk_external" path="Images/tmp"/>
   <root-path name="opensdk_root" path=""/>
</paths>

三、初始化配置

1.初始化SDK

加入以下代码在创建登录的那个activtiy下,不然无法拉起QQ应用的登录界面,至于官方文档所说的需要用户选择是否授权设备的信息的说明,这里通用的做法是在应用内部声明一个第三方sdk的列表,然后在里面说明SDK用到的相关设备信息的权限

Tencent.setIsPermissionGranted(true, Build.MODEL)

2.创建实例

这部分建议放在全局配置,这样可以实现登录异常强制退出等功能

/**
* 其中APP_ID是申请到的ID
* context为全局context
* Authorities为之前provider里面配置的值
*/
val mTencent = Tencent.createInstance(APP_ID, context, Authorities)

3.开启登录

在开启登录之前需要自己创建一个 UIListener 用来监听回调结果(文档没讲怎么创建的,找了好久的demo)这里的代码为基础的代码,比较容易实现,目前还没写回调相关的代码,主要是为了快速展示效果

open class BaseUiListener(private val mTencent: Tencent) : DefaultUiListener() {
private val kv = MMKV.defaultMMKV()
override fun onComplete(response: Any?) {
if (response == null) {
"返回为空,登录失败".showToast()
return
}
val jsonResponse = response as JSONObject
if (jsonResponse.length() == 0) {
"返回为空,登录失败".showToast()
return
}
"登录成功".showToast()
doComplete(response)
}

private fun doComplete(values: JSONObject?) {

}
override fun onError(e: UiError) {
Log.e("fund", "onError: ${e.errorDetail}")
}

override fun onCancel() {
"取消登录".showToast()
}
}

建立一个按钮用于监听,这里进行登录操作

button.setOnClickListener {

if (!mTencent.isSessionValid) {
//判断会话是否有效
when (mTencent.login(this, "all",iu)) {

//下面为login可能返回的值的情况
0 -> "正常登录".showToast()
1 -> "开始登录".showToast()
-1 -> "异常".showToast()
2 -> "使用H5登陆或显示下载页面".showToast()
else -> "出错".showToast()
}
}
}

这边对mTencent.login(this, "all",iu)中login的参数做一下解释说明

mTencent.login(this, "all",iu)
//这里Tencent的实例mTencent的login函数的三个参数
//1.为当前的context,
//2.权限,可选项,一般选择all即可,即全部的权限,不过目前好像也只有一个开放的权限了
//3.为UIlistener的实例对象

还差最后一步,获取回调的结果的代码,activity的回调,这边显示方法已经废弃了,本来想改造一下的,后面发现要改造的话需要动sdk里面的源码,有点麻烦就没有改了,等更新

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//腾讯QQ回调,这里的iu仍然是相关的UIlistener
Tencent.onActivityResultData(requestCode, resultCode, data,iu)
if (requestCode == Constants.REQUEST_API) {
if (resultCode == Constants.REQUEST_LOGIN) {
Tencent.handleResultData(data, iu)
}
}
}

至此,已经可以正常登录了,但还有一件我们开发者最关心的事情没有做,获取的用户的数据在哪呢?可以获取QQ号吗?下面将为大家解答这方面的疑惑。

四、接入流程以及相关代码

首先回答一下上面提出的问题,可以获得两段比较关键的json数据,一个是 login 的时候获取的,主要是token相关的数据,还有一段就是用户的个人信息的 json 数据,这些都在 UIListener 中进行处理和获取。第二个问题能不能获取QQ号,答案是不能,我们只能获取与一个与QQ号一样具有唯一标志的id即open_id,显然这是出于用户的隐私安全考虑的,接下来简述一下具体的登录流程

1.登录之前检查是否有token缓存

  • 有,直接启动主activity

  • 无,进入登录界面

判断是否具有登录数据的缓存

//这里采用微信的MMKV进行储存键值数据
MMKV.initialize(this)
val kv = MMKV.defaultMMKV()
kv.decodeString("qq_login")?.let{
val gson = Gson()
val qqLogin = gson.fromJson(it, QQLogin::class.java)
QQLoginTestApplication.mTencent.setAccessToken(qqLogin.access_token,qqLogin.expires_in.toString())
QQLoginTestApplication.mTencent.openId = qqLogin.openid
}

检查token和open_id是否有效和token是否过期,这里采取不同于官方的推荐的用法,主要是api失效了或者是自己没用对方法,总之官方提供的api进行缓存还不如MMKV键值存login json来的实在,也很方便,这里建议多多使用日志,方便排查错误

//这里对于uiListener进行了重写,object的作用有点像java里面的匿名类
//用到了checkLogin的方法
mTencent.checkLogin(object : DefaultUiListener() {
override fun onComplete(response: Any) {
val jsonResp = response as JSONObject

if (jsonResp.optInt("ret", -1) == 0) {
val jsonObject: String? = kv.decodeString("qq_login")
if (jsonObject == null) {
"登录失败".showToast()

} else {
//启动主activity

}
} else {
"登录已过期,请重新登录".showToast()
//启动登录activity

}
}

override fun onError(e: UiError) {
"登录已过期,请重新登录".showToast()
//启动登录activity

}

override fun onCancel() {
"取消登录".showToast()
}
})

2.进入登录界面

在判断session有效的情况下,进入登录界面,对login登录可能出现的返回码做一下解释说明

Login.setOnClickListener {
if (!QQLoginTestApplication.mTencent.isSessionValid) {
when (QQLoginTestApplication.mTencent.login(this, "all",iu)) {
0 -> "正常登录".showToast()
1 -> "开始登录".showToast()
-1 -> {
"异常".showToast()
QQLoginTestApplication.mTencent.logout(QQLoginTestApplication.context)
}
2 -> "使用H5登陆或显示下载页面".showToast()
else -> "出错".showToast()
}
}
}
  • 1:正常登录

    这个就无需做处理了,直接在回调那里做相关的登录处理即可

  • 0:开始登录

    同正常登录

  • -1:异常登录

    这个需要做一点处理,当时第一次遇到这个情况就是主activity异常消耗退回登录的activity,此时在此点击登录界面的按钮导致了异常情况的出现,不过这个处理起来还是比较容易的,执行强制下线操作即可

    "异常".showToast()
    mTencent.logout(QQLoginTestApplication.context)
  • 2:使用H5登陆或显示下载页面

    通常情况下是未安装QQ等软件导致的,这种情况无需处理,SDK自动封装好了,这种情况会自动跳转QQ下载界面

同样的有出现UIListener就需要调用回调进行数据的传输

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//腾讯QQ回调
Tencent.onActivityResultData(requestCode, resultCode, data,iu)
if (requestCode == Constants.REQUEST_API) {
if (resultCode == Constants.REQUEST_LOGIN) {
Tencent.handleResultData(data, iu)
}
}
}

3.进入主activity

这里需要放置一个按钮执行下线操作,方便调试,同时这里需要将之前的token移除重新获取token等数据的缓存

button.setOnClickListener {
mTencent.logout(this)
val kv = MMKV.defaultMMKV()
kv.remove("qq_login")
//返回登录界面的相关操作
"退出登录成功".showToast()
}

至此,其实还有一个很重要的东西没有说明,那就是token数据的缓存和个人信息数据的获取,这部分我写的登录的那个UIlistener里面了,登录成功的同时,获取login的response的json数据和个人信息的json数据

4.获取两段重要的json数据

  • login 的json数据

    这个比较容易,当我们登录成功的时候,oncomplete里面的response即我们想要的数据

    override fun onComplete(response: Any?) {
    if (response == null) {
    "返回为空,登录失败".showToast()
    return
    }
    val jsonResponse = response as JSONObject
    if (jsonResponse.length() == 0) {
    "返回为空,登录失败".showToast()
    return
    }
    //这个即利用MMKV进行缓存json数据
    kv.encode("qq_login",response.toString())
    "登录成功".showToast()
    }
  • 个人信息的数据

    这个需要在login有效的前提下才能返回正常的数据

    //首先需要用上一步获取的json数据对mTencent进行赋值,这部分放在doComplete方法中执行
    private fun doComplete(values: JSONObject?) {
    //利用Gson进行格式化成对象
    val gson = Gson()
    val qqLogin = gson.fromJson(values.toString(), QQLogin::class.java)
    mTencent.setAccessToken(qqLogin.access_token, qqLogin.expires_in.toString())
    mTencent.openId = qqLogin.openid
    Log.e("fund",values.toString())
    }

    创建一个get_info方法进行获取,注意这里需要对mTencent设置相关的属性才能获取正常获取数据

    private fun getQQInfo(){
       val qqToken = mTencent.qqToken
       //这里的UserInfo是sdk自带的类,传入上下文和token即可
       val info = UserInfo(context,qqToken)
       info.getUserInfo(object :BaseUiListener(mTencent){
           override fun onComplete(response: Any?){
               //这里对数据进行缓存
               kv.encode("qq_info",response.toString())          
          }
      })
    }

5.踩坑系列

这里主要吐槽一下关于腾讯的自带的session缓存机制,当时是抱着不用自己实现缓存直接用现成的机制去看的,很遗憾这波偷懒失败,这部分session的设置不知道具体的缓存机制,只知道大概是用share preference实现的,里面有saveSession,initSession,loadSession这三个方法,看上去很容易的样子,然后抱着这种心态去尝试了一波,果然不出意外空指针异常,尝试修改了一波回调的顺序仍然空指针异常,折腾了大概三个多小时,放弃了,心态给搞崩了,最终释然了,为什么要用腾讯提供的方法,这个缓存自己实现也是相当的容易,这时想到了MMKV,两行代码完成读取,最后只修改了少数的代码完成了登录的token的缓存机制,翻看demo里面的实现,里面好像是用这三种方法进行实现的,可能是某个实现机制没有弄明白,其实也不想明白,自己的思路比再去看demo容易多了,只是多了一个json的转对象的过程,其他的没有差别。所以建议后来者直接自己实现缓存,不用管sdk提供的那些方法,真的有点难用。

五、总结

总之这次完成QQ接入踩了许多的坑,不过幸好最终还是实现了,希望腾讯互联这个sdk能够上传github让更多的人参与和提供反馈,不然这个文档说是最差sdk体验也不为过。下面附上这次实现QQ登录的demo的github地址以及相关的demo apk供大家进行参考,大概总共就400行代码左右比官方的demo好很多,有问题欢迎留言

https://github.com/xyh-fu/QQLoginTest.git

作者:wresource
来源:juejin.cn/post/7072878774261383176

收起阅读 »

2022年终总结——迷茫摆烂

前言 如果要用两个关键词来概括2022全年,我认为是“迷茫”和“摆烂”。迫于对未来的迷茫无措,我写下了这些文字。原本没有打算发出来,但是仔细想想,这又何尝不是一种逃避?所以我发了出来,希望老哥们可以在看完文章后帮我指引前路,不在迷茫。标志性事件太多,麻烦认出来...
继续阅读 »

前言


如果要用两个关键词来概括2022全年,我认为是“迷茫”和“摆烂”。迫于对未来的迷茫无措,我写下了这些文字。原本没有打算发出来,但是仔细想想,这又何尝不是一种逃避?所以我发了出来,希望老哥们可以在看完文章后帮我指引前路,不在迷茫。标志性事件太多,麻烦认出来的哥哥,可以给我个面子。


YX_M8PDCNTLL}6_(KD9S5.gif


求助


我有两个路线,不知道走哪个。一个是考研,一个是学习,来年跳槽。求指导。


考研的话,北京的学校难度咋样?非全日制的会被承认吗?毕业后大约29岁了,还来得及吗?


学习的话,应该学什么技术呀?应该深入还是全栈?


流水账


关于工作


我在2021年毕业,在年底进入公司,是安卓开发岗位。一直到2月过年那段时间,我摸清了自己的工作内容,接手了以往的项目。我们部门只有我一个安卓开发,其他部门不熟,但是好像也没有安卓岗。没有人教,所以我一直都是靠前任的代码和百度来学习的。我的定位就是应对项目可能会出现的安卓需求,不涉及核心。看似多余,不过工资低(6000),公司养得起。


公司氛围很轻松,朝九晚六。我们同事一直都在忙项目,有点顾不上我。偶尔有人给我派发学习任务,我简单学完之后,面对大把时间就开始手足无措。好多事想干,又不知道先干什么,于是我开始摆烂逃避。看资讯、看小说、刷论坛、刷贴吧,一混就是一天,很爽,深陷其中。


日子就这样混到了三月底。部长看我太闲了,让我跟其中一个项目。这是和安卓风马牛不相及的项目,我也很慌,就开始百度教程学习,慢慢不再混日子。后来正式上手,太难,摸不到门路。只好向同事要了一份他们的做参考,慢慢的上手了,越做越快。于是,在知道截止日期的情况下,我开始摆烂了。玩一会儿,做一点儿。但我还是在截止前一天完成了,耗时一个月。


四月底,部长好像觉得我做的很可以,又给我派了一部分任务。这部分更难,更多。我一开始遇到了点难题,卡了我好久。在百度疯狂的cv了一周之后才解决。然后我又开始摆烂了,这次摆烂的更狠。再加上后面因为疫情开始居家,于是越加肆无忌惮。发觉的时候已经过了20多天了,也居家半个月了。然后我就只能疯狂加班补,最终还是在截止那天完成了,耗时一个半月。


那是六月初,我们刚结束隔离,重新上班。我感到很内疚,因为其他人的工作量是我的两倍,而且部长也催过我。然后我决定发愤图强,然后接过了学习任务。学习完了之后,开始摆烂。后面给我安排了新的任务,需要用到上面学习的内容,然后我就一边学习,一边做,一边摸鱼。


然后就摸鱼摸到了八月,两个月过去了。这时候给我安排了一个安卓项目,我当时真的差点喜极而泣。但是同时,我也真的很慌。因为我除了毕设,就没有再单独开发一个安卓项目了。然后我就结束摆烂,一边偶尔摸鱼,一边努力干活。


一直努力到十月,我终于完成了。其实现在仔细想想,难度也不是很难。之所以耗费两个月时间,多半是因为我经常前半天摸鱼,后半天干活吧。然后就又没事了,放假回来之后就提了点意见让我修改。然后我就又不知道干啥了,就继续摸鱼摆烂。


摆烂到十一月,我们又因为疫情被迫居家了,我直接毫无顾忌的开始疯玩了。后面有新冠什么的,写在生活里了。总之,一直摆烂到现在,偶尔完成一下工作。


fd9dfcb8-c8ee-4ef2-8096-f17e70b117b9.webp


总结


总的来说,基本上就是有任务的时候就干活,不忙的时候就迷茫摆烂。


每天都过得浑浑噩噩的,白天迷茫摆烂,夜里焦虑失眠。在公司也没什么师傅来指导,东学一点西学一点的。整个技术成长过程特别碎片化,知识结构不成体系,技术深度严重不足。稍微遇到一些开放性的有难度的问题,就没有足够的信心搞定,产生明显的畏难情绪和自卑心理,觉得自己技不如人,开始逃避,继而摆烂。


每次开会,同事的任务都很高深,到了我就是学习***,感觉自己像个边缘人。


我看不清前面的路该怎么走,未来的技术路线该怎么制定,最近几个月要关注什么,学习什么,自己身上那么多的问题,要优先解决哪一个。


66ae641f-e905-42b6-bdbc-5f771d837f94.webp


关于生活


因为公司朝九晚六,所以我空闲时间还不少。基本上就是下班回家打游戏、做饭、看小说。我几乎没有自控能力,所以一玩就玩到半夜两三点再睡,因此经常迟到。不过因为公司制度,所以还没有扣过钱。但是睡的太少,前半天只能靠摸鱼看论坛来维持精神。然后午休睡2个小时,后半天在工作。这个状态会在有任务的时候减轻,在没任务的时候加重。


在四月份的时候换了房子。原先住的隔断,虽然双方都很安静,但是因为不隔音,对自控能力极差的我来说简直是折磨(对方是女的,色色不方便)。所以我搬到了离公司很近,房租1800的7平米厨房改的小房间。之前地铁通勤要300元,起床要7:30,这次我血赚。


换了房子之后就更加没有节制了。因为房间小,也就不再做饭了,每天都是打游戏、看小说、刷视频来回换。但是,每当很晚的时候,我好像没事做了该睡觉了,我就感觉空虚茫然。感觉之前做的那些都没有让我真的快乐起来。明明到了该睡觉的时间了,可是心里却很慌,觉得我还没有真的放松一下,可是我不知道做什么。然后就只能继续干刚才那些,等我不知不觉的睡着为止。所以我第二天就经常起不来床,一天都很累,可是回到家里又不知道干什么才能真正地放松自己,只能重复以前的生活,恶性循环了。


打游戏、看小说、刷视频,在一开始确实是快乐的,但是时间长了就开始坐牢了。我明明已经感觉不到快乐了,却不舍得离去,总会觉得接下来一定还有地方可以继续获得快乐,下一个视频、故事情节一定更有趣。因为,如果我退出去了,我就又会迷茫空虚,不知所措。我下意识的想要逃避,不想面对,因为逃避真的有用。但是也因为没有解决根本问题而恶性循环,这都是自己的决定导致的结果。尼采说所有过去的事情是你的意志选择的结果,积极接受,因为这是你的命运。


转而到了六月,中介给我埋得雷爆了。我看房的时候是毛坯房,中介说后面都会给安装空调的,但是房东没给安。一整个夏天我都是靠两个风扇熬过去的。偶尔受不了了,就去隔壁屋蹭一蹭空调。


我在家里读小说只读长度100到200章之间的,因为我会控制不住一直读下去,沉浸于人物中无法自拔,导致晚睡。我一开始喜欢读言情重生种田爽文,毕竟我从小从农村长大,很有代入感。后面也开始读其他类型的,但是也逃不过言情种田爽文这一块。对于从小贫穷吃不起饭,社交能力有问题的我来说,我真的需要飞黄腾达,也需要一个住在心里的人。我一直都梦想有个像小说主角一样有能力的人,带我走出泥潭。后来读的小说多了,我也走出来了,知道自己为什么沉迷了。现实做不到的,只能靠小说了。


e0363488-c421-45eb-91a7-2dc6f046aee2.gif


我走出小说是七月份的事情了。因为迷茫却又不再读小说,所以我只能多开游戏。我那时候手里在玩三款游戏,玩完正好0点,奖励一下正好睡觉。但是现实不会这么如人愿。一开始还好,时间一长,游戏玩的和上班一样了。玩完还是很累,想休息。可是不知道咋休息,只能刷短视频到3点。


后来我加入了一个游戏交流群,群里人好多都是成年土豪,却又很和善。他们每天当黑奴带本,却没有怨言。人的成长来源,或是经历,或是社交,或是阅读。在高中之后,我终于迎来了一个稳定且长期的社交途径。我跟着一起谈论游戏,也会谈论自己的生活,以求指点。但是成长是缓慢的,我还没有摆脱困境,就进入了一个更大的困境。


c080671a-f61e-410a-a0e2-b9b32aa81af2.webp


一切的转折点是十二月初,我被诈骗了。


具体经过很蠢,我就不发了。我攒了几年的积蓄全没了,还背上了1万的网贷。虽然第一时间就报了警,但是我知道,我这4万多,回不来了。出了警察局我才发现,我要交下个季度的房租了。我之前经常有朋友向我借钱,我圣母心蛮重的,看不得别人受苦,所以我陆陆续续的借出去了2万。所以事发之后,我第一时间联系他们。但是,就要回来一千,甚至有些垃圾人都不回消息了。


我很难过,不敢和家里说,只能看看能不能靠12月要发的工资撑过去,实在不行就网贷。


过了几天,我例行和父母打微信视频。这时候我才知道我爸爸和几个亲戚来北京的工地干活了,而且阳了。他们买不到药,只能困在工地里面干熬着。我很心疼,第二天就很早去楼下药铺排队买药,想着给他们送过去。买完药已经十一点了,想着先回家吃点饭,顺便问问地址。结果正吃着饭呢,突然就感觉特别难受,特别冷,还十分不清醒。我爸爸也说不用给他们送药,他们都快好了。我赶紧给自己贴了好多的暖宝宝,盖着被子睡觉了。虽然措施不少,但还是觉得冷的不行。我以为我是排队的时候,穿的少了,被冻着了。好不容易睡着,再醒来还是冷。我觉得不对了,一看体温计,39.5度。我居然就这么阳了。刚买的药,全给自己用了。


我躺在床上,很委屈,难受的想哭。我到手就6000块钱,我在这刚工作一年攒下4万容易吗?我除了房租水电等必要的花销,几乎不消费的。之前借我钱的那些人,有些还是在大学的时候借的。我家里在农村也是最底下的那一档了,我大学四年努力打零工去实习,省吃俭用才存下了一点儿钱。我穿着高中买的烂衣服,他们还向我借钱,我以为他们可能真缺钱,才借给他们的。谁知道他们转头拿去买苹果,花天酒地了。在我真的需要帮助的时候,却只有一个人还了钱。


我把我被诈骗要不回钱阳了这些事说在了群里,求到了许多的指点。我之后就去要钱了。也不是没有进展,要回来一部分。只是,我说话的时候,他们苹果手机的灵动岛一直在跳动,很刺眼。仿佛在嘲笑我,我现在吃饭的工具,用的还是2017年花4000买的电脑。手机也是三年前花了2000买的。我对自己并不好,对别人那么好干嘛,又不是土豪。人不为己天诛地灭。


我不敢乱消费。我知道我看不清自己,现在消费主义盛行,谁知道这些需求到底是不是真正的自己的需求。就像是,我已经单身26年了,早就分不清自己是真的喜欢,还是只想色色了。更何况,我家里真的穷,在我毕业挣钱之后才搬离我住了20多年的土坯房。我爸爸已经50多了,也去不了几天工地搬砖了,我只能指望我自己。


我经常在想,我死了之后会发生什么。我死了以后,这世界的所有事情全都和我无关了。我的后事如何处理,亲人会不会难过,过了几年他们还会不会记得我?我的后代会如何发展,会越来越好还是最终都消散了?我们国家呢,会越来越昌盛,还是功亏一篑,全族消失?那地球呢,会不会最终被太阳吞噬?那银河最终会不会被黑洞吞噬?那宇宙最终会不会热寂呢?还是宇宙最终会变成一个互相吞噬而成的大黑洞,最终大爆炸?


一般我想到这里就不敢继续往下想了。但在我阳的最严重的时候,在我以为我快死了的时候,我反而胆子大了起来。我不敢想下去是因为我不甘心。很多事情我注定无法亲眼见证,很多事情我可以却没有尝试过。也许这就是会有很多人相信轮回的原因吧。人总是会因为各种原因而产生遗憾。贫苦者寄希望于来生过上富裕的生活,痴情者寄希望于来生可以再续前缘。我想明白了,我不甘心,我想见见那些美丽的风景,我想尝尝那些神奇的美食,我想试试双人到底比单人爽在哪里,我想让父母过上好的生活。我也想享受人生,享受生活。然后我打开京东,下单,余额不足。


74f7d066-c1a4-4bfc-87ee-21b1871d38dc.webp
 
从大四开始,也就是我开始步入社会的时候,每个冬天我都会因为轻信他人而受到严重损失。算上这次,已经是三次了。金额越来越大,后果越来越严重。第一次借钱给他人却要不回来,第二次被实习公司坑,第三次被诈骗。


我开始仔细反思自己究竟为什么被骗,因为我一开始就觉得有问题,我却一直跟着对方的脚步走,我当时想暂停,我为什么没有暂停?我性格有问题,我喜欢被动,胆小怕事。我不会主动找人聊天,我只会等被人来找我。我不会找事情做,只会等事情来找我。所以我一直跟着骗子的脚步走,知道有问题却不敢停止。


为什么不主动,为什么胆小怕事呢?


我从小家里就很穷,住土坯房里,睡一张炕上,吃院子里种的菜。平时穿堂哥剩下来的衣服,冬天才有新衣服,才有肉吃。我最苦的时候是高中的时候。那时候家里变故也多,我一个月只能拿200块钱在食堂吃饭。每天吃馒头蘸酱,偶尔吃3块钱的白菜和西红柿。不知道为什么,我明明一直吃不饱,体重却越来越高。家里人也开始说我,觉得我乱花钱。河北的高中压力太大了,我每天晚上睡不着,哗哗的掉头发。精神身体双层打压之下,我变得胆小懦弱。因为我没有试错空间,我要是错了,就真的完了。


我那时候真是给我饿坏了,现在还有影响,我已经分不清我是不是吃饱了,我只能等吃不下了才会停手,给我多少,我就一定都吃了。高中饿的时间太久导致的,已经是潜意识了。


我被动是因为我没有一个目标,啥都可以,所以就开始等着别人摆布。我挺胸无大志的。我以前觉得,能吃饱有住的地方就行,攒点钱以后回村,毕竟成长期我一直吃不饱。
从我进入大学之后,我突然就没事干了。之前一直都有一双无形的手推着我走向大学,现在这双手消失了。高中是我最痛苦的时光,所以我厌学了,开始摆烂。


我分不清好坏,分不清冷热,分不清是否吃饱,分不清是否喜欢,分不清是否需要。推着我的手消失了,我就不知道应该怎么办了。我曾经想天降主角,帮我制定规则,推着我走。但是,这是不可能的。因为我没有一个目标,所以我迷茫,所以我被动。所以有人推我时,我即使知道那是骗子,我也会下意识的由着他推着走。


我也知道自己胆小怕事的原因是自身不够强大。阳的时候,我多想有个人,可以带我走出泥潭,仿佛小说主角一般。但是,我阳过了也没人来。我最后才知道,我只能靠自己。有人指点我说,不管你目标是什么,不管最终能不能实现,你都要先写出来,先说出来。只有这样你才会为之努力,想办法去实现他。畏畏缩缩在心里不敢提出来,只会让人踌躇不前,最终会导致觉得自己会做不到,不断否定自己。长此以往,就会变得胆小怕事。


归根到底,我一直都在忽视自己真正的感受,从来没有认清自己。明白自己真正想要什么很难,人生得意须尽欢,我这时候才真正地明白。


既然不知道自己真正想要什么,那就多尝试吧,全都试试就知道了。我想尝试很多的新鲜事物,我想过优质的生活,我想强大起来不再懦弱,我不想父母劳累了。这些都需要钱。
 


所以,我当前阶段的主要目标就是搞钱!


40971e2f-7101-4962-99e9-6b902d951d8b.gif


不幸中的万幸,我被骗之后,大数据知道我缺钱,疯狂给我发垃圾短信,让我网贷。然后我看到了保险的短信,然后我想起来我买了好几年的保险。一切很顺利,3小时就理赔成功了。给了我2.7万,可以回家过年了。


但是回家之后又出了很多事。就不细谈了,说起来就生气。我以为天底下还是好人多,但这次为数不多的坏人都让我家碰上了。


总结


不抱怨,三思后行,学会享受人生,努力赚钱!!
明白自己真正想要什么很难,人生得意须尽欢,我想尝试很多的新鲜事物,我想过优质的生活,我想强大起来不再懦弱,我不想父母劳累了。这些都需要钱。


所以,我当前阶段的主要目标就是搞钱!


2023年计划


如果说“迷茫”和“摆烂”是2022全年的两个关键词,那么我希望2023全年的两个关键词是“尝试”和“积累”。


既然不知道自己真正想要什么,那就多尝试吧,全都试试就知道了。我想尝试很多的新鲜事物,我想过优质的生活,我想强大起来不再懦弱,我不想父母劳累了。这些都需要钱。


既然不知道路,那就多尝试吧,把想尝试的都去试试,不再压抑自己。人生得意须尽欢!


找到路之后就一路积累,一路走下去吧。


06dc8620-8db5-4a13-b4fc-d4f7cce108be.gif


关于工作


我有两个路线,不知道走哪个。一个是考研,一个是学习,来年跳槽


我今年26了,要是考研的话,毕业就得29。程序员吃青春饭,我怕到时候跟不上了。而且北京的研究生不知道好不好考。


学习积累方面也不知道学啥。因为公司需求不大,所以我想往全栈那边走一下。我今年的计划是,先学uni-app,再学flutter。系统学习一下安卓,更新一下技术,并使用新技术重新写一遍自己的毕设。学一下主流的后端技术,重新写一遍自己的毕设的后端。


关于生活


明白自己真正想要什么,多多尝试新鲜事物。


开始健身,至少今年要减20斤肥,变成健康的身体。新冠太可怕了,我被折磨怕了。


学习一下护肤品相关知识,准备尝试找对象,不想单身了。(我看他们都开始捣鼓化妆了,难道现在流行男生化妆了吗)


规律生活,不要再熬夜了。


后言


想说的太多了,文字总是太过苍白,无法表达万一。


其实由于各种原因,时间并不站在我这里。我已经26了,这个时候才开窍似乎是晚了。


但是,最好的开始时刻就是当下。


我之前也有想改变的时候,但是“晚了”这两个字让我给自己判了死刑。不停否定自己,不再进步。


这次,我不想再放弃了。


这次,我不想再放弃了。


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

7年老菜鸟平凡的2022

前言 嗨,大家好,我是希留。一个被迫致力于成为一名全栈开发的菜鸟。 2022年对于大多数人而言是难忘的一年,受疫情影响、经历行业寒冬。裁员、失业等情况总是笼罩着本就焦虑不安的生活。 而我的2022,用一个关键词就可以概括:“平凡”。既没有升职加薪,也没有被裁...
继续阅读 »

前言




嗨,大家好,我是希留。一个被迫致力于成为一名全栈开发的菜鸟。


2022年对于大多数人而言是难忘的一年,受疫情影响、经历行业寒冬。裁员、失业等情况总是笼罩着本就焦虑不安的生活。


而我的2022,用一个关键词就可以概括:“平凡”。既没有升职加薪,也没有被裁失业。就像是一潭湖水,没有什么波澜。可即便如此,也得默默的努力着。下面就来盘点一下属于我的2022。


工作技能上


付出


这一年,本本分分的完成工作上的功能开发,业余时间则在几个技术平台上更新一些文章,几乎都是一篇文章手动同步发送几个平台。


累计在CSDN上写了31篇文章,掘金上更新了28篇文章,公众号更新了26篇文章,头条号更新了26篇文章。


除了更新文章外,还利用业余时间帮朋友开发了一款租房小程序,包含管理后台以及微信小程序。目前还在开发当中,感兴趣的朋友也可以体验一下。后台体验账号: test/123456
小程序端:


64ccc50073fa86f853ef92a04564fe7.jpg


收获


CSDN


csdn平台就只收获了访问数据以及粉丝数的增加。
image.png


掘金


在掘金平台除了收获一些访问数据的增加外,还切切实实的参与创作活动薅到一些马克杯、抱枕、现金等羊毛,在此感谢掘金平台让我体会到了写作真的能够带来价值,感谢~
image.png


0e14788d1e1954b94630a725f71e53e.jpg


微信公众号


微信公众号收获了一些粉丝。


image.png


头条号


头条号收获了一些粉丝,以及微薄的收益。


image.png


副业拓展上


付出


不知道是受大环境影响还是年龄大了,老是会有各种焦虑。所以也萌生了想要开展副业的想法,于是参加了几个付费社群,也跟着入局实践了两个项目,一个是闲鱼电商,一个是外卖cps。


有朋友入局过这种副业项目的也可以评论区交流一下。


收获


咸鱼上的GMV是2w多,利润有3k多,有这个收益还是比较满意的,希望可以越来越好。


244ace2f758021d6758331fbd6ba0eb.png


外卖CPS虽然也有一点收益,但是还不够微信300块的认证费,这个项目算是费了。
image.png
总的来说,想要做副业也不是那么容易的,虽然眼前有一点点小收益,但是想要放大太难了。


2023未来展望



  • 完成租房小程序的开发并上线。

  • 更新不少于25篇技术类文章

  • 寻找一个更适合技术人员的副业项目

  • 完成人生大事


总结


好了,以上就是我的2022总结和未来展望了,感谢大家的阅读。


生活如果不宠你,更要自己善待自己。这一路,风雨兼程,就是为了遇见最好的自己,如此而已。


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

错的不是世界,是我

楔子 "咕咚,咕咚,咕咚",随着桶装水一个个气泡涌上来,我的水杯虽已装满,但摸鱼太久的我却似木头般木讷,溢出的水从杯口流了下来,弄湿了我的new balance。问:此处描写代表了作者什么心情(5分)。 阳春三月,摸鱼的好季节,我拿起水杯小抿了一口,还是那个甘...
继续阅读 »

楔子


"咕咚,咕咚,咕咚",随着桶装水一个个气泡涌上来,我的水杯虽已装满,但摸鱼太久的我却似木头般木讷,溢出的水从杯口流了下来,弄湿了我的new balance。问:此处描写代表了作者什么心情(5分)


阳春三月,摸鱼的好季节,我拿起水杯小抿了一口,还是那个甘甜的味道。怡宝,永远的神。这个公司虽然没人陪我说话,工作量也不饱和,但是只要它一天不换怡宝,我便一直誓死效忠这个公司。我的水杯是个小杯,这样每次便能迅速喝完水走去装水,提高装水频率,极大提升摸鱼时长,我不自暗叹我真是一个大聪明。装水回到座位还没坐下,leader便带来一个新来的前端给每位组员介绍,我刚入职三个月,便又来了一位新人。leader瞥了我一眼,跟新人介绍说我也是刚入职的前端开发做题家,我看了新人一眼,脑海闪过几句诗词——眼明正似琉璃瓶,心荡秋水横波清面如凝脂,眼如点漆,她呆呆的看着我,我向她点头示意,说了声,你好。她的名字,叫作小薇小月。


天眼


"小饿,过来天若有情找我一下",钉钉弹出来一条消息,正是HRBP红姐发来的,我的心里咯噔了一下,我正在做核酸,跟她同步后她让我做完核酸找她。其实下楼做核酸的时候我看到跟我负责同个项目的队友被红姐拉去谈话,公司找我聊天,除了四年前技术leader莫名奇妙帮我加薪有兴趣可看往期文章,其他都没有发生过好事。我的心里其实已经隐隐约约知道了什么事情,一边做着核酸一边想着对策,一边惶恐一边又有几分惊喜,心里想着不会又要拿大礼包了吧,靠着拿大礼包发家致富不是梦啊


来到天若有情会议室,我收拾了一下心情,走了进去,"坐吧"。红姐冷冷的说了一声,我腿一软便坐了下来。"知道我找你是什么原因吗?"红姐率先发问,"公司是要裁员吗?"我直球回击。红姐有点出乎意料笑了一下,"啪"的一声,很快,我大意了,没有闪。一堆文件直接拍到了桌面,就犹如拍在我的脸上。"这是你上个月的离开工作位置时长,你自己核对一下,签个名"。


我震惊。没想到对面一上来就放大。我自己的情况我是知道的,早上拉个屎,下午喝杯茶,悠然混一日。加上嘘嘘偶尔做做核酸每天离开工作岗位大约2个小时左右。"为什么呀,你入职的时候表现不是这样子的呀,为什么会变成这样呢?是被带坏了吗?没有需求吗?还是个人原因?"。既然你诚心诚意发问,那我就大发慈悲告诉你吧。


003b22e3873764bac0452c69c7b835fd.jpeg



为什么呢?



从入职新公司后,我的心感觉就不属于这里,公司指派的任务都有尽心尽责完成,但是来了公司大半年,做了一个项目上线后没落地便夭折,另外一个项目做了一半被公司业务投诉也立刻中断,我没有产出,公司当我太子一样供着。自己从上家公司拿了大礼包后,机缘巧合又能快速进入新的公司,其实自己是有点膨胀的,到了新公司完成任务空闲时便会到掘金写写小说,晚上回家杀杀狼人。有时动一下腰椎,也会传来噼里啪啦的声响,似乎提醒我该去走走了。不过不学无术,游手好闲,的确是我自己的问题。每天摸两小时,资本家看了也会流泪。当然,这些都是马后炮自己事后总结的


"是我个人原因"。虽说极大部分归于没有需求做,但是没有需求做也不代表着能去摸鱼,而且更不能害了leader,我心里明白,这次是我错了。太子被废了。我在"犯罪记录"上面签了字,问了句如何处理,回复我说看上面安排。我出来后发现有两个同事也来询问我情况,我也一五一十说了,发现大家都是相同问题。我默默上百度查了下摸鱼被裁的话题,发现之前tx也有过一次案例,虽然前两次都是败诉,最后又胜诉了。


我晚上回去躺在床上翻来覆去睡不着觉,心中似乎知道结局,避无可避,但错在我身,这次的事件就当做是一个教训,我能接受,挨打立正。闲来无事打开BOSS刷了又刷,岗位寥寥无几,打开脉脉,第一条便是广州找工作怎么这么难啊,下面跟着一大群脉友互相抱团取暖,互相安慰,在寒冬下,大家都知道不容易,大家都互相鼓励,互相给出希望,希望就像一道道暖风,吹走压在骆驼身上的稻草,让我们在时间的流逝下找到花明


第二天,红姐让我去江湖再见会议室。"其实是个坏消息啦,X总容忍不了,这是离职协议,签一下吧"。我看了厚厚的离职协议,默不作声,"签个人原因离职后背调也可以来找我,我这边来协助安排"。弦外之音声声割心,但其实我心里也明白,我也没有底气,不如利索点出来后看看能不能尽快找个工作。


晚宴


leader知道我们几个明天last day后,拉个小群请我们吃饭。也是在这次宴席中,leader透露出他也会跟着我们一起走,我大为吃惊,随后leader便娓娓道来,我知道了很多不为人知的秘密。这次总共走了四个人,都是前端,其中涉及了帮派的斗争,而我们也成为斗争中的牺牲品。我一边听着leader诉说公司的前尘往事,一边给各位小伙伴倒茶,心里也明白,就算内斗,如果自己本身没有犯错,没有被抓到把柄,其实也不会惹祸上身。leader也跟我说因为他个人原因太忙没有分配给我适合的工作量,导致我的确太闲,也让我给简历给他帮忙内推各种大厂,我心里十分感激。


期间有位小伙伴拍着我肩膀说,"我知道你是个很好的写手,但是这些东西最好不要写出来"。我一愣,他接着说,之前有位前端老员工识别到是你的文章,发出来了。凉了,怪不得我变成砧板的鱼肉,原来我的太子爽文都有可能传到老板手里了。我突然心里一惊,问了一句不会是因为的Best 30 年中总结征文大赛才导致大家今晚这场盛宴吧?leader罢了罢手,说我想多了。我也万万想不到,我的杰作被流传出去,可能点赞的人里面都藏着CEO。就怕太子爽文帮我拿到了电热锅,却把我饭碗给弄丢了。不过我相信,上帝为你关上一扇门,会为你打开一扇窗。


不过掘金的奖牌着实漂亮,谢谢大家的点赞,基层程序员一个,写的文章让大家有所动容,有所共鸣,实乃吾之大幸。


WechatIMG312.jpeg


天窗


自愿离职后的我开始准备简历,准备复习资料,同时老东家也传来裁员消息。心里不禁感叹,老东家这两次裁员名单,都有我的名字。我刷了下boss,投了一份简历,便准备面试题去了,因为我觉得我的简历很能打,但是面试的机会不多,每一次面试都是一个黄金机会,不能再像上次一样错过。当天一整天都很down,朋友约出来玩,我也拒绝了,但是朋友边邀请边骂边安慰我,我想了一下就当放松一下了,于是便出去浪了一天。第二天睡醒发现两个未接来电,回拨过去后是我投递简历的公司打来的,虽然我没有看什么面试题,但是好在狼人杀玩的够多,面对着几位面试官夸夸其谈,聊东南西北,最终也成功拿下offer。虽然offer一般,但在这个行情下,我一心求稳,便同意入职,所以也相当于无缝衔接。对这位朋友也心怀感激,上次也是他的鼓励,让我走出心中的灰暗,这次也是让我在沮丧中不迷失自我。那天我玩的很开心,让我明白工作没了可以再找,错误犯了可以改回来,但人一旦没了信心迷失方向,便容易坠入深渊。


THE END


其实很多人都跟我说,互联网公司只要结果,这次其实我没犯啥毛病,大家都会去摸鱼。我经过几天思考我也明白,不过,有时候真要从自己身上找下原因,知道问题根本所在,避免日后无论是在工作还是生活中,都能避免在同一个地方再次跌倒。其实大多时候,错的不是世界,而是我


过了几天,leader请了前端组吃一顿他的散伙饭,因为他交接比较多,所以他走的比较晚。菜式十分丰富,其中有道羊排深得我心,肥而不腻,口有余香,难以言喻。小月坐在我的隔壁,在一块羊排上用海南青金桔压榨滴了几滴,拍了拍我的肩膀,让我试一下。我将这块羊排放入口中,金桔的微酸带苦,孜然的点点辛辣,羊排本身浓郁的甜味,原来,这就是人生啊。仔细品尝后,我对小月点了点头,说了声谢谢。


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

2022年还好有你-flutter

第一次参加年终总结,还有点紧张呢!一直都是随遇而安的佛系心态,也许就是那种“我已经努力了”的心态,还写什么总结走形式呢,一直都是抗拒的。但看了好多大神的总结,让我感触良多。一个成功的人士,都是有自己的完整规划,包括职业,人生,技术等。突然让我明白,一个自律的人...
继续阅读 »

第一次参加年终总结,还有点紧张呢!一直都是随遇而安的佛系心态,也许就是那种“我已经努力了”的心态,还写什么总结走形式呢,一直都是抗拒的。但看了好多大神的总结,让我感触良多。一个成功的人士,都是有自己的完整规划,包括职业,人生,技术等。突然让我明白,一个自律的人才更加容易成功。


一、工作


2022年一步一个脚印的走着,平淡又踏实的。需求一个接着一个,有时候是一个接着两个。移动端iOS和Android就孤家寡人,加上开发时间由整个项目进度弹性调整。奋笔疾书已经赶不上了。从去年开始研究的flutter在虎年,还是帮上了大忙。虽然在开始研究时,收到了领导的警告。但是当提高了100%的开发效率时,领导应该摸着XX庆幸了。


公司新开了一个业务线,界面和功能繁多,又涉及到iOS和Android。真的是为flutter量身打造啊。果断的采用,并持续的输入。使用体验能有个原生的80%吧。还是欣慰的。


突然回忆不起太多的开发内容了,应该都是缝缝补补的较多吧。


记得用半个月的时间编写了一个单车的模拟器来支持一个特殊的城市。这个经历让我非常有成就感。因为没有实车和中控,导致后端开发和测试人员没法操作。如果有一个模拟器那么可以解决燃眉之急。大家都不看好的时候,我觉得可以试下。拿起python进行业务逻辑的堆积。哈哈,是堆代码了。不求质量,只求能用。 最后磕磕碰碰的帮上了大忙。


熟悉了python后,又用它进行了很多脚本的编写,查询异常车辆,异常任务,批量处理数据等等。还是非常好用。但是真的只是一个工具,想再深入的时候,没有业务的需求,太难。只能学到够用了。


虎年说在哪方面学习的最深入,那应该还是flutter了。将张风捷特烈的文章都看了一遍,每一篇文章也都点赞了。学到了不少的东西。特别是被他的那种深入学习,超级自律的行为打动。继续向他学习。


虎年工作中最高兴的事,应该还是3年前申请的专利下来了。看到纸质证书的时候,真的是感觉到自己孩子出生一样。当拿到我的专利专利奖金时,立刻下单了最新款的mac book pro。人生中最贵的电脑,用着真的是舒服。


二、心态


工作一直保持着勤勤恳恳,也拥抱变化,一个人扛起整个移动端。从长远来看,学的东西太广了,没有自己的拿手技术,不是一个理想的状态。但能够学着新东西,又可以在项目中马上投入使用。这种自由感,真的是太爽了。也许是我没有太大的抱负,就会用忙碌来麻痹自己。


有两样事,还是一直都有坚持。(1)英语(2)锻炼。


此生英语虐我千百遍,我待英语如初恋。每天都会坚持背诵单词,每天必定在《不背单词》签到,复习之前的单词然后学习新的单词。不能说毫无进步吧。至少已经不怕英语了,懂的单词越来越多,看文章越来越快了。英文的系统和工具,已经没有障碍了。坚持吧,也许真的是少了一点天赋。


还坚持过一段时间的听力,随着项目压力,慢慢地被遗忘。


锻炼身体,也有一直在做。选择了跳绳,买了几根绳,买了手环。想把这个作为一辈子的运动。从开始的跳几百个,到能跳1k多。也很有成就感。


三、输出


文章


虎年的输出,只能说靠运气了。碰到什么问题解决下,然后做一个记录。没有特别去写文章。阅读量最大的文章也是flutter的iOS的编译问题。当时写这个文档的时候仅仅用了1个小时吧。虽然解决问题花了几天。每天看到那么多关注和点赞,偷偷地笑了。


有想过好好写点文章,但是任何东西感觉用几句话就能说完的,写一篇文章太啰嗦了,还是发一个沸点吧。


写这个Flutter实现闪电效果文章,当时的想法是从来没有参加过活动,要不要试试。证明下自己是不是也是可以的。用一天来构思,一天来实现代码,一天来编写文章。然后找了同事给我点赞。最后顺利拿到马克杯,很开心。


四、 源码学习


虎年也试着开始学习源码,将dio、provider、 flutter(part)、 dart(part)、 FlutterUnit等源码,一行行的看了。最尴尬的是每一行都看懂了,整体的看不懂。觉得每一个地方都很简单,整体框架怎么样,没有思路。是不是设计模式没研究透,又去学习了一遍设计模式。但是发现设计模式也学的一知半解。我明白我还有很长的路要走啊。


试着编写了自己的FlutterSnippet,把看到好玩的好用的收集起来。以后在项目中可以用到。


五、 生活


生活只能说只有家庭了。只从有了女儿后,在家里那么全是围绕着她。把时间都花在她身上,让她能够感受到满满的爱,能够自信又快乐的成长。希望疫情过去后,能够陪着她看遍大江南北吧。


在B站关注了好多有意思的UP主。删除了抖音和微博,因为这些无味的内容,让我每次睡前都很后悔。学习自己想学习的,自己来把握自己的视频,感觉充实起来了。


看到那么多宝藏UP主,才知道人生原来可以那么丰富多彩,而不仅仅只有学习。希望我的人生我的生活,也多姿多彩起来。


六、 2023年展望


对兔年,虽然如水般的平静。但也想向大神们学习,定一个目标。


将flutter的源码看一遍。


至于能够看懂多少,还真的是心里没底。用我最喜欢的话“美丽新世界”,来表达对这个世界一直充满好奇吧。


总结


虎年感谢有你--flutter,是你让我明白,不断去尝试新的技术,保持学习。让自己的路越走越广。


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

不过是享受了互联网的十年红利期而已。

你好呀,我是歪歪。 去年的最后一天,我在 B 站发布了这个视频: 我真没想到这个长达一个小时的视频的播放量能到这么多,而且居然是一个投币比点赞和收藏还多的视频。 评论区加上弹幕,有上千条观众的留言。每一条留言都代表一个观众的感受,里面极大部分的感受总结起来大...
继续阅读 »

你好呀,我是歪歪。


去年的最后一天,我在 B 站发布了这个视频:



我真没想到这个长达一个小时的视频的播放量能到这么多,而且居然是一个投币比点赞和收藏还多的视频。


评论区加上弹幕,有上千条观众的留言。每一条留言都代表一个观众的感受,里面极大部分的感受总结起来大多是表示对于我个人这十年经历感叹和羡慕,但是我是觉得十年的时间这么长,谁还不能提炼出几两故事和几段感悟呢?


觉得感叹的,只不过是在我的视频里面看到了几分自己的影子。觉得羡慕的,只不过是以另外一种我羡慕的方式生活着。


还是有人说是逆袭,我并不觉得这是逆袭。逆袭一般是说绝地反击的故事,但是我觉得这十年,我还没有真正的走到过“绝地”,更多的只是随着时代随波逐流,我个人的努力,在时代的浪潮前,微不足道,只不过在一系列的机缘巧合之下,我使劲的方向恰好和浪潮的方向一致而已。


我当时没有想到一个好的词语来形容这个“和浪潮的方向一致”,所以过年期间我也一直在仔细的思考这个问题。


直到过年期间,我坐在火炉前听家里的长辈聊天,一个长辈问另外一个晚辈:你什么时候把女朋友带回来给我们看看呢?


晚辈戏谑的回答说:我现在自己都过的不好呢,怕是没有女孩子愿意跟我哦。


长辈说:我以前嫁过来的时候,你爷爷以前还不是啥都没有,就一间土巴屋,一个烂瓦房。结婚嘛,两个人一起努力不就把日子过好了。


我当时好想说一句:那个时代过去了,现在不一样了。


然而终究还是没说出口,但是就在把这句话咽下去的瞬间,我想明白了前面关于“浪潮”的问题,其实就一句话:


我只不过是有幸享受到了时代的红利罢了。有时候的看起来让人羡慕的人、成功的人,只不过是享受到了时代的红利罢了,与个人的努力程度关系真的不大。


我说的时代的红利,就是互联网技术、计算机专业野蛮发展的这十年光景。


在视频里面,我说了一句话:我是被调剂到计算机专业的。


然后有一些弹幕表示非常的震惊:



是的,在 2012 年,计算机专业并不是一个被看好的热门专业,甚至有点被“淘汰”的感觉。


我记得那一年录取之后,给一个亲戚说是就读计算机专业,他说:怎么学了个这专业啊,以后每个家庭里面都会有一台计算机,到时候人人都会使用计算机,还学它干啥?


这句话虽然现在看起来很搞笑,但是在那个时候,我还没有接触到它的时候,我觉得很有道理。


虽然我是调剂到“计算机”的,但是前提也得是我填报志愿的时候填写了“计算机专业”,对吧。


所以问题就来了:我当年是怎么高瞻远瞩,怎么深思熟虑,怎么推演计算,怎么预测未来,想着要填报一个计算机专业呢?


为了回答这个问题,我今年回到老家,专门把这个东西翻了出来:



这是我高考结束那年,学校发的 4 本和填报志愿相关的书,书里面主要的内容就是过去三年各个批次,各个学校,各个专业的报考人数、录取人数、录取最低分数、录取平均分数、录取最高分数的信息统计:



我当年填报志愿,就是通过翻阅这四本书来找到自己可以填报的大学。但是我的高考志愿特别好填,因为我高考成绩只超过二本线 13 分,所以我直接看二本院校里面每年卡着分数线招收学生的学校就可以了。在这个条件下,没有多少学校可以选择。


最后录取我的大学,是 2012 年那一年刚刚由专科学校升级为二本院校的一所大学。所以那一年是它第一次招本科生,没有过往的数据可以参考,报它的原因是因为我感觉它刚刚从专科升级为本科,录取分数应该不会太高。


填报志愿的时候一个学校可以填写六个专业,刚好它也只有六个本科专业,所以我就按照报纸上的顺序,挨个填写,而且还勾选了“服从调剂”。


而这六个专业,我也通过前面的书翻到了:



当时对于这六个专业,我完全没有任何了解。根本不知道要学习什么内容,更加不知道毕业之后会从事什么工作。


后来入校之后我才知道,报材料成型及控制工程和机械电子工程专业的人最多,计算机科学与技术由于报的人没有报满,所以我被调剂过去了。


可以肯定的说,如果当年这个学校没有计算机的本科专业,我就不会走上计算机的道路。


其实我填报志愿的状态,和当年绝大部分高考学生的状态一样,非常的茫然。在高中,学校也只教了高考考场上要考的东西,为了这点东西,我们准备了整整三年。但是现在回头去看,如何填报志愿,其实也是一件非常值得学习了解的事情,而不是高考结束之后,学校发几本资料就完事的。


我当时填报志愿时最核心的想法是,只要有大学录取就行了,至于专业什么的,不重要。


在志愿填报指南的书里面,我发现有大量的篇幅站在 2012 年视角去分析未来的就业形势。



这部分,我仔细的读了一遍,发现关于计算机部分提到的并不多,只有寥寥数语,整体是持看好态度,但是大多都是一些正确的“废话”,对于当年的我来说,很难提炼出有价值的信息,来帮助我填写志愿。


后来得知被计算机录取了之后的第一反应是,没关系,入校之后可以找机会转专业,比如转到机械。


为什么会想着机械呢?


因为那一年,或者说那几年,最火的专业是土木工程,紧随其后的大概就是机械相关的专业:



而这个学校没有土木专业,那就是想当然的想往人多的,也是学校的王牌专业“机械”转了。


计算机专业,虽然也榜上有名,但是那几年的风评真的是非常一般,更多的是无知,就像是亲戚说的那句:以后人人都有一台计算机,你还去学它干啥?


我也找到了一份叫做《2011年中国大学生就业报告》的报告,里面有这样一句话:



真的如同弹幕里面一个小伙伴说的:土木最火,计算机下水道。


所以我在十年前被调剂到计算机专业,也就不是一个什么特别奇怪的事情了。


你说这是什么玩意?


这里面没有任何的高瞻远瞩、深思熟虑、推演计算、预测未来,就是纯粹的运气。


就是恰好站在时代的大潮前,撅着屁股,等着时代用力的拍上那么一小下,然后随着浪花飘就完事了吗?


我也曾经想过,如果我能把它包装成一个“春江水暖鸭先知”的故事,来体现我对于未来精准的预判就好了,但是现实情况就是这么的骨感和魔幻,没有那么多的预判。


所以有很多人,特别是一些在校的或者刚刚毕业的大学生,通过视频找到我,来请教我关于职业发展,关于未来方向,关于人生规划的问题。



说真的,我有个屁的资格和能力来帮你分析这些问题啊。我自己这一摊子事情都没有搞清楚,我的职业前路也是迷雾重重,我何德何能给别人指出人生的方向?


当然,我也能给出一些建议,但是我能给出的所有的回复,纯粹是基于个人有限的人生阅历和职业生涯,加上自己的一些所见所闻,给出的自己角度的回答。


同样的问题,你去问另外一个人,由于看问题的角度不同,可能最终得出的答案千差万别。


甚至同样的职场相关的问题,我可以给你分析的头头是道,列出一二三四点,然后说出每一点的利益得失,但是当我在职场上遇到一模一样的问题时,我也会一时慌张,乱了阵脚,自然而然的想要去寻求帮助。


在自媒体的这三年,我写过很多观点输出类的文章,也回答过无数人的“迷茫”。对于这一类求助,有时是答疑,常常是倾听,总是去鼓励。


我并不是一个“人生导师”,或者说我目前浅薄的经验,还不足以成为一个“人生导师”,我只不过是一个有幸踩到了时代红利的幸运儿而已。


在这十年间,我踩到了计算机的红利,所以才有了后面看起来还算不错的故事。


踩到了 Java 的红利,所以才能把这个故事继续写下去。


踩到了自媒体的红利,所以才有机会把这些故事写出来让更多的人看到。


现在还有很多很多人摩肩擦踵的往计算机行业里面涌进来,我一个直观的感受就是各种要求都变高了,远的就不说了,如果是三年前我回到成都的时候,市场情况和现在一样的话,我是绝对不可能有机会进入到现在这家公司,我只不过是恰好抓住了一个窗口期而已。


还有很多很多的人,义无反顾的去学 Java,往这个卷得没边的细分领域中冲的不亦乐乎,导致就业岗位供不应求,从而企业提升了面试难度。我记得 2016 年我毕业的时候,在北京面试,还没有“面试造火箭”的说法,当年我连 JVM 是啥玩意都不知道,更别提分布式相关的技术了,听都没听过。然而现在,这些都变成了“基础题”。


还有很多人,看到了自媒体这一波流量,感觉一些爆款文章,似乎自己也能写出来,甚至写的更好。或者感觉一些非常火的视频,似乎自己也能拍出来,甚至拍的跟好。


然而真正去做的话,你会发现这是一条“百死一生”的道路,想要在看起来巨大的流量池中挖一勺走,其实很难很难。


但是如果把时间线拉回到 2014 年,那是公众号的黄金时代,注册一个公众号,每天甚至不需要自己写文章,去各处搬运转载,只需要把排版弄好看一点,多宣传宣传,然后坚持下去,就能积累非常可观的关注数量,有关注,就有流量。有流量,就有钱来找你。从一个公众号,慢慢发展为一个工作室,然后成长为一个公司的故事,在那几年,太多太多了。


诸如此类,很多很多的现象都在表明则一个观点:时代不一样了。


我在刚刚步入社会的时候,看过一本叫做《浪潮之巅》的书,书里面的内容记得不多了,但是知道这是一本把计算机领域中的一些值得记录的故事写出来的好书。


虽然书的内容记得不多了,但是书的封面上写的一段话我就很喜欢。


就用它来作为文章的结尾吧:



近一百多年来,总有一些公司很幸运地、有意识或者无意识地站在技术革命的浪尖之上。一旦处在了那个位置,即使不做任何事,也可以随着波浪顺顺当当地向前漂个十年甚至更长的时间。在这十几年间,它们代表着科技的浪潮,直到下一波浪潮的来临。这些公司里的人,无论职位高低,在外人看来,都是时代的幸运儿。因为,虽然对一个公司来说,赶上一次浪潮不能保证其长盛不衰;但是,对一个人来说,一生赶上一次这样的浪潮就足够了。一个弄潮的年轻人,最幸运的,莫过于赶上一波大潮。



以上。








如果我这篇文章结束在这个地方,那么你先简单的想一想,你看完之后那一瞬间之后的感受是什么?


会不会有一丝丝的失落感,或者说是一丢丢的焦虑感?


是的,如果我的文章就结束在这个地方,那么这就是一篇试图“贩卖焦虑”的文章。


我在不停的暗示你,“时代不一样了”,“还是以前好啊”,“以前做同样的事情容易的多”。


这样的暗示,对于 00 后、90 后的人来说,极小部分感受是在缅怀过去,更多的还是让你产生一种对当下的失落感和对未来的焦虑感。


比如我以前看到一些关于 90 年代下海经商的普通人的故事。就感觉那个时代,遍地是黄金,处处是机会,只要稍稍努力就能谱写一个逆天改命的故事,继而感慨自己的“生不逢时”。


只是去往回看过去的时代,而没有认真审视自己的时代,当我想要去形容我所处的时代的时候,负面的形容词总是先入为主的钻进我的脑海中。


我之前一直以为是运气一直站在我这边,但是我真的是发布了前面提的到视频,然后基于视频引发了一点讨论之后,我才开始更加深层次的去思考这个问题,所以我是非常后知后觉的才感受到,我运气好的大背景是因为遇到了时代的红利。


要注意前面这一段话,我想强调的是“后知后觉”这个词。这个词代表的时间,是十年有余的时间。


也就是说在这十年有余的时间中,我没有去刻意的追求时代的红利、也没有感知到时代的红利。


这十年间,概括起来,我大部分时间只是做了一件事:努力成长,提升自我。


所以在我的视频的评论区里面还有一句话出现的频率特别高:越努力,越幸运。


我不是一个能预判未来的人,但是我并不否认,我是一个努力的人,然而和我一样努力,比我更加努力的人也大有人在。


你要坚信,你为了自己在社会上立足所付出的任何努力是不可能会白费的,它一定会以某种形式来回报你。


当回报到来的时候,也许你认为是运气,其实是你也正踩在时代的红利之上,只不过还没到你“后知后觉”的时候,十年后,二十年后再看看吧。


在这期间,不要囿于过去,不要预测未来,你只管努力在当下就好了。迷茫的时候,搞一搞学习,总是没错的。



(特么的,这味道怎么像是鸡汤了?不写了,收。)



最后,用我在网上看的一句话作为结尾吧:



我未曾见过一个早起、勤奋,谨慎,诚实的人抱怨命运不公;我也未曾见过一个认真负责、努力好学、心胸开阔的年轻人,会一直没有机会的。



以上就是我对于处于“迷茫期”的一些大学生朋友的一点点个人的拙见,也是我个人的一些自省。


共勉。


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

Android 通过productFlavors实现多渠道打包

在日常开发中,可能会遇到同一份代码,需要根据运营需求打出不同包名、不同图标、不同名称的Apk,发布到不同的渠道中。Android Studio提供了便捷的多渠道打包实现方法productFlavors。 本文介绍一下使用productFlavors来实现多渠道...
继续阅读 »

在日常开发中,可能会遇到同一份代码,需要根据运营需求打出不同包名、不同图标、不同名称的Apk,发布到不同的渠道中。Android Studio提供了便捷的多渠道打包实现方法productFlavors


本文介绍一下使用productFlavors来实现多渠道打包。


创建productFlavors



  • 添加Dimension


在app包下的build.gradle中的android闭包下,添加flavorDimension,代码如下:


android {
...

// 方式1
getFlavorDimensionList().add('example_value')

// 方式2
flavorDimensions "example_value"
}

两种方式选择一种即可,方式1有代码补全提示,方式2没有。



  • 创建productFlavor


在app包下的build.gradle中的android闭包下,创建productFlavors,代码如下:


android {
...

productFlavors {
// 原始渠道
origin{
// 这里的值与前面flavorDimensions设置的值保持一致
dimension 'example_value'
}

// 示例渠道
exampleFlavor {
// 这里的值与前面flavorDimensions设置的值保持一致
dimension 'example_value'
}
}
}

网上找到的相关文章都说productFlavor中需要配置dimension,但是在尝试的过程中发现,如果只添加了一个flavorDimensions,那么productFlavor中的dimension可以不用特别声明(我的gradle版本为7.6,AGP为7.4.1)。


构建完后可以在Build Variants中看到已配置的变体,如图:


屏幕截图(8).png

渠道包参数配置


打渠道包时,根据需求可能会需要配置不同参数,例如App的名称、图标、版本信息,服务器地址等。



  • 配置不同的签名信息


如果需要使用不同的签名文件,可以在app包下的build.gradle中的android闭包下配置signingConfigs,代码如下:


android {
signingConfigs {
origin {
keyAlias 'expampledemo'
keyPassword '123456'
storeFile file('ExampleDemo')
storePassword '123456'
}

exampleFlavor {
keyAlias 'exampledemoflavor'
keyPassword '123456'
storeFile file('ExampleDemoFlavor.jks')
storePassword '123456'
}
}

flavorDimensions "example_value"

productFlavors {
origin{
signingConfig signingConfigs.origin
}

exampleFlavor {
signingConfig signingConfigs.exampleFlavor
}
}
}

需要注意的是signingConfigs必须在productFlavors前面声明,否则构建会失败。



  • 配置包名、版本号


productFlavors中可以配置渠道包的包名、版本信息,代码如下:


android {
...

defaultConfig {
applicationId "com.chenyihong.exampledemo"
versionCode 1
versionName "1.0"
...
}

productFlavors {
origin{
...
}

exampleFlavor {
applicationId "com.chenyihong.exampledflavordemo"
versionCode 2
versionName "1.0.2-flavor"
}
}
}

origin渠道表示的是原始包,不进行额外配置,使用的就是defaultConfig中声明的包名以及版本号。


效果如图:


origin


1676109942922.png


exampleFlavor


1676110092402.png



  • 配置BuildConfig,字符串资源


productFlavors中配置BuildConfig或者resValue,可以让同名字段,在打不同的渠道包时有不同的值,代码如下:


android {
...
productFlavors {
origin{
buildConfigField("String", "example_value", "\"origin server address\"")
resValue("string", "example_value", "origin tips")
}

exampleFlavor {
buildConfigField("String", "example_value", "\"flavor server address\"")
resValue("string", "example_value", "flavor tips")
}
}
}

配置完后重新构建一下项目,就可以通过BuildConfig.example_value以及getString(R.string.example_value)来使用配置的字段。


效果如图:


origin


1676110403151.png


exampleFlavor


1676110302147.png



  • 配置manifestPlaceholders


有些三方SDK,会在Manifest中配置meta-data,并且这些值跟包名大概率是绑定的,因此不同渠道包需要替换不同的值,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
...
>

<meta-data
android:name="channel_value"
android:value="${channel_value}"/>

....
</application>
</manifest>

android {
...
productFlavors {
origin{
manifestPlaceholders = [channel_value: "origin channel"]
}

exampleFlavor {
manifestPlaceholders = [channel_value: "flavor channel"]
}
}
}

效果如图:


origin


1676109422577.png


exampleFlavor


1676109298513.png



  • 配置不同的依赖


不同渠道包可能会引用不同的三方SDK,配置了productFlavors后,可以在dependencies中区分依赖包,代码如下:


dependencies {
// origin 包依赖
originImplementation("com.google.code.gson:gson:2.10.1")

// exampleFlavor包依赖
exampleFlavorImplementation("com.google.android.gms:play-services-auth:20.4.1")
}

示例:


FlavorExampleActivity中同时导入Gson包和Google登录包,效果如下:


origin


1676108237739.png

exampleFlavor


1676108290585.png

  • 配置不同的资源


在app/src目录下,创建exampleFlavor文件夹,创建与main包下一样的资源文件夹,打渠道包时,相同目录下同名的文件会自动替换,可以通过这种方式来实现替换应用名称和应用图标。


1676111680651.png


效果如图:


Screenshot_20230211_183741.png

示例Demo


按照惯例,在示例Demo中添加了相关的演示代码。


ExampleDemo github


ExampleDemo gitee


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

用做猪脚面的方式,理解下模版方法模式

模版方法模式 前言 模版方法模式,是行为型设计模式的一种。这个模式的使用频率没有那么高,以至于之前从来没有了解过该模式。不过兄弟们也不用怕,这个模式其实还是比较简单的。等会咱们举个例子,来理解一下这个模式。 介绍 概念理解 模版方法模式,个人理解是,将类中的一...
继续阅读 »

模版方法模式


前言


模版方法模式,是行为型设计模式的一种。这个模式的使用频率没有那么高,以至于之前从来没有了解过该模式。不过兄弟们也不用怕,这个模式其实还是比较简单的。等会咱们举个例子,来理解一下这个模式。


介绍


概念理解


模版方法模式,个人理解是,将类中的一些方法执行顺序进行排序。其中的部分方法可以被重写。排序后的方法就是模版方法。排序后的类就是模版类。这种代码设计思路就是模版方法模式


菜谱:猪脚面


上面的描述可能有点抽象。那么咱就换一个讲法来说一下这个模式。


从前呢,在京海市有一条街叫旧厂街,那里呢有一个菜市场,菜市场里有一个卖鱼的小老板他叫高启强。他呢有一个弟弟妹妹。兄妹三人啊从小就喜欢吃猪脚面。但是由于家里穷,所以三人只够吃一碗面。妹妹高启兰吃猪脚,弟弟高启盛吃面,而高启强就只能喝面汤。


由于的确穷,他就去找饭店老板要了一份菜谱。饭店老板看他可怜就给了他一份猪脚面的菜谱,具体如下:



  1. 把水烧开

  2. 放面条

  3. 放猪脚

  4. 放佐料

  5. 把面煮熟


他兴高采烈的按照菜谱做了一份猪脚面,给弟弟妹妹吃。可是结果却让他失望了。因为猪脚面的味道出了问题。


他去找了老板,老板对他说,阿强啊,我给你的菜谱肯定没问题,味道不对一定是哪个环节出错了。


于是他又给老板做了一遍。当他放完佐料的时候。老板立刻叫住了他,对他说。阿强,你其他的步骤都没有错,但是放佐料这一步和我有些不一样。


这一步这里你应该要放的是酱油和老抽,再用盐和鸡精调味。可这里你只用了醋来调味,所以味道不对。高启强满脸通红的对老板说,对不起啊老板,我家太穷类没有那些调理所以只能用醋代替了。


在上面这个例子中,这里面的菜谱就是模板也可以说是框架
菜谱执行顺序可以被看作是模板方法。而且这里的执行顺序是固定无法被改变的。
执行顺序无法改变,但是具体的做菜步骤却是可以被重写的。比如说放佐料。


例子中的高启强正是由于这一步的不同,导致他做出的猪脚面和老板的口味不一致。


2023春晚


上面这个可以看作是模版方法模式的一个简单举例。接下来咱们再举个有代码的例子加深下对模版方法模式的印象。


春晚模版类


SpringFestivalGala规定了春晚必须遵循的节目流程。这个代码中的start方法,可以看作是模版方法模式中最重要的一环,因为他就是规定了其他方法调用顺序模版方法



  1. 开场白

  2. 唱歌

  3. 跳舞

  4. 小品

  5. 难忘今宵


由于不同卫视的节目顺序都遵循这套模版。而且最后的节目难忘今宵是春晚保留节目,所以该节目必须所有春晚保持一致,具体代码如下所示:


/**
* Author(作者):jtl
* Date(日期):2023/2/10 20:05
* Detail(详情):春晚流程(春晚模版 )
*/
public abstract class SpringFestivalGala {
public void start(){
prologue();
song();
dance();
comedySketch();
unforgettableTonight();
}
//开场白
public abstract void prologue();

//歌曲节目
public abstract void song();

//小品节目
public abstract void comedySketch();

//舞蹈节目
public abstract void dance();

//难忘今宵
private void unforgettableTonight(){
System.out.println("结尾:难忘今宵");
}
}
复制代码

上面的代码中,不同的春晚,有着不同的小品舞蹈等节目,所以需要SpringFestivalGala的子类需要重写这几个方法。但是难忘今宵是所有春晚共同的节目。因此可以复用
start方法就可以看作是模版方法。它里面的节目执行顺序固定的无法被改变。


辽视春晚


辽宁春晚继承了春晚的固定模版。具体代码如下:


/**
* Author(作者):jtl
* Date(日期):2023/2/10 20:42
* Detail(详情):辽宁春晚
*/
public class SpringFestivalGalaOfLiaoning extends SpringFestivalGala{
@Override
public void prologue() {
System.out.println("开场白:欢迎来到,2023年,辽宁卫视春晚现场");
}

@Override
public void song() {
System.out.println("歌曲:孙楠,谭维维-追光");
}

@Override
public void comedySketch() {
System.out.println("小品:宋小宝-杨树林:非常营销");
}

@Override
public void dance() {
System.out.println("舞蹈:舞蹈-欢庆中国年");
}
}
复制代码

央视春晚


央视春晚同样遵循春晚的传统模版。有着开场白,歌曲等精彩的演出。尤其是小品初见照相馆一经播出,一己之力推动年轻人的离婚率,简直是今年节目之最!
央视春晚的具体代码如下:


/**
* Author(作者):jtl
* Date(日期):2023/2/10 20:53
* Detail(详情):CCTV 央视春晚
*/
public class CCTVSpringFestivalGala extends SpringFestivalGala{
@Override
public void prologue() {
System.out.println("开场白:欢迎来到,2023年,央视春晚的现场");
}

@Override
public void song() {
System.out.println("歌曲:邓超-好运全都来");
}

@Override
public void comedySketch() {
System.out.println("小品:于震-初见照相馆");
}

@Override
public void dance() {
System.out.println("舞蹈:辽宁芭蕾舞团:我们的田野上");
}
}

复制代码

客户端代码


调用这两个类的客户端代码


/**
* Author(作者):jtl
* Date(日期):2023/2/10 20:04
* Detail(详情):模版方法模式客户端
*/
public class Client {
public static void main(String[] args) {
CCTVSpringFestivalGala cctv = new CCTVSpringFestivalGala();
cctv.start();

System.out.println("----------------分割线----------------");

SpringFestivalGalaOfLiaoning liaoning = new SpringFestivalGalaOfLiaoning();
liaoning.start();
}
}
复制代码

运行结果


结果如图所示
模版方法模式.png


模版方法模式的模版



  1. 有一个固定的模版类A,它是一个抽象类

  2. 模版类A里有一些方法,这些方法里有需要子类重写的抽象方法

  3. 有一个模版方法,它里面有着这些方法的调用顺序。这个顺序是不能被改变的,也是模版方法模式的核心

  4. 子类继承模版类A,重写它的抽象方法


后记总结


至此,模版方法模式就算是介绍完毕了。细心的小伙伴可能发现了,模版方法模式的模版如果要扩展的话,就必须改了啊,他这违反了开闭原则啊。


没错,这是这个模式的一个缺陷。从模版方法模式的定义来看,它的概念就是给其他类提供一套固定的执行流程,这个执行流程就是模版方法。其他类只能修改其中的方法,不能修改执行流程,即不能修改模版方法。所以它从定义上就不存在修改执行流程这一可能。可能有点强行洗白,但是这也是一种解释方式。


还是那句话,对于设计模式来说,没有固定的套路。毕竟它只是人们经过长时间总结出来的代码经验。所以千万别被所谓的设计模式框架所拘束,只要符合要求,有利阅读扩展就是好的代码。


如果喜欢请点个赞,支持一下。有错误或不同想法请及时指正哈。辛苦您看到这里,下篇文章再见哈,👋👋👋


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

一起掌握Kotlin协程基础

前言 在平时的开发中,我们经常会跟线程打交道,当执行耗时操作时,便需要开启线程去执行,防止主线程阻塞。但是开启太多的线程,它们的切换就需要耗费很多的资源,并且难以去控制,没有及时停止或者控制不当便很可能会造成内存泄露。并且在开启多线程后,为了能够获取到计算结果...
继续阅读 »

前言


在平时的开发中,我们经常会跟线程打交道,当执行耗时操作时,便需要开启线程去执行,防止主线程阻塞。但是开启太多的线程,它们的切换就需要耗费很多的资源,并且难以去控制,没有及时停止或者控制不当便很可能会造成内存泄露。并且在开启多线程后,为了能够获取到计算结果,我们需要采用回调的方式来回调结果,但是回调多了,代码的可读性变得很差。kotlin协程是运行在线程之上,我们使用它时能够很好地去控制它,并且在切换方面,它消耗的CPU和内存大大地降低,它不会阻塞所在线程,可以在不用使用回调的情况下便可以直接获取计算结果。


正文


协程程序


GlobalScope.launch { // 在后台启动一个新的协程并继续
   println("hello Coroutine")
}
//输出:hello Coroutine

GlobalScope调用launch会开启一个协程。


协程的组成



  • CoroutineScope

  • CoroutineContext:可指定名称、Job(管理生命周期)、指定线程(Dispatchers.Default适合CUP密集型任务、Dispatchers.IO适合磁盘或网络的IO操作、Dispatchers.Main用于主线程)

  • 启动:launch(启动协程,返回一个job)、async(启动带返回结果的协程)、withContext(启动协程,可出阿如上下文改变协程的上下文)


作业


当我们开启协程后,可能需要对开启的协程进行控制,比如在不再需要该协程的返回结果时,可将其进行取消。好在调用launch函数后,会返回一个协程的job,我们可利用这个job来进行取消操作。


val job = GlobalScope.launch {
   delay(1000)
   println("world")
}
println("hello")

job.cancel()

//输出结果为:hello

可能有的同学会觉得奇怪为什么world没有输出,原因是当调用cancel时,会对该协程进程取消,也就是不再执行了直接停止。下面再看一个join方法:


val job = GlobalScope.launch {
   delay(1000)
   println("world")
}
println("hello")
job.join()
//输出结果:
//hello
//world

join方法会等待该协程执行结束。


超时


当一个协程执行超时,我们可能需要取消它,但手动跟踪它的超时可能会觉得麻烦,所以我们可以使用withTimeout方法来进程超时跟踪:


withTimeout(1300) {
   repeat(10){i->
       println("i-->$i")
       delay(500)
  }
}
//i-->0
//i-->1
//i-->2
//抛出TimeoutCancellationException异常

这个方法在设置的超时时间还没完成时,抛出TimeoutCancellationException异常。如果我们只是单纯防止超时而不抛出异常,则可使用:


val wton = withTimeoutOrNull(1300){
   repeat(10){i->
       println("i-->$i")
       delay(500)
  }
}

println("end -- $wton")
//i-->0
//i-->1
//i-->2
//end -- null

挂起函数


当我们在launch函数中写了很多代码,这看上去并不美观,为了可以抽取出逻辑放到一个单独的函数中,我们可以使用suspend 修饰符来修饰一个方法,这样的函数为挂起函数:


suspend fun doCalOne():Int{
   delay(1000)
   return 5
}

挂起函数需要在挂起函数或者协程中调用,普通方法不能调用挂起函数。


我们通过使用两个挂起函数来获取它们各自的计算结果,然后对获取的结果进一步操作:


suspend fun doCalOne():Int{
   delay(1000)
   return 5
}
suspend fun doCalTwo():Int{
   delay(1500)
   return 3
}

coroutineScope {
       val time = measureTimeMillis {
           //同步开始,需要按顺序等待
           val one = doCalOne()
           val two = doCalTwo()
           println("one + two = ${one + two}")
      }
       println("time is $time")
}
//one + two = 8
//time is 2512

我们可以看到,计算结果正确,说明能够正常返回,而且总共的耗时是跟两个方法所用的时间的总和(忽略其他),那我们有没有办法让两个计算方法并行运行能,答案是肯定的,我们只需使用async便可以实现:


coroutineScope {
       val time = measureTimeMillis {
           //异步开始
           val one = async{doCalOne()}
           val two = async{doCalTwo()}
           //同步开始,需要按顺序等待
           println("one + two = ${one.await() + two.await()}")
      }
       println("time is $time")
}
//one + two = 8
//time is 1519

我们可以看到,计算结果正确,并且所需时间大大减少,接近运行最长的计算函数。


async类似于launch函数,它会启动一个单独的协程,并且可以与其他协程并行。它返回的是一个Deferred(非阻塞式的feature),当我们调用await方法才可以得到返回的结果。


async有多种启动方式,下面实例为懒性启动:


coroutineScope {
   //调用await或者start协程才被启动
   val one = async(start = CoroutineStart.LAZY){doCalOne()}
   val two = async(start = CoroutineStart.LAZY){doCalTwo()}

   one.start()
   two.start()
}

我们可以调用start或者await来启动它。


结构化并发


虽然协程很轻量,但它运行时还是需要耗费一些资源,如果我们在使用的过程中,忘记对它进行引用,并且及时地停止它,那将会造成资源浪费或者出现内存泄露等问题。但是一个一个跟踪(也就是使用返回的job)很不方便,一个两个还好管理,但是多了却不方便管理。于是我们可以使用结构化并发,这样我们可以在指定的作用域中启动协程。这点跟线程的区别在于线程总是全局的。大致如图(图片):


image.png


在日常开发中,我们会经常开启网络请求,有时候需要同时发起多个网络请求,我们想要的是在挂起函数中启动多个请求,当挂起函数返回时,里边的请求都执行结束,那么我们可以使用coroutineScope 来进行指定一个作用域:


suspend fun twoFetch(){

   coroutineScope {
       launch {
           delay(1000L)
           doNetworkJob("url--1")
      }
       launch { doNetworkJob("url--2") }
  }
}

fun doNetworkJob(url : String){
   println(url)
}
//url--2
//url--1

coroutineScope等到在其里边开启的所有协程执行完成再返回。所以twoFetch不会在coroutineScope内部所启动的协程完成前返回。


当我们取消协程时,会通过层次结构来进行传递的。


suspend fun errCoroutineFun(){

   coroutineScope {
       try {
           failedCorou()
      }catch (e : RuntimeException) {
           println("fail with RuntimeException")
      }
  }

}

suspend fun failedCorou() {

   coroutineScope {

       launch {
           try {
               delay(Long.MAX_VALUE)
               println("after delay")
          } finally {
               println("one finally")
          }
      }

       launch {
           println("two throw execption")
           throw RuntimeException("")
      }
  }
}
//two throw execption
//one finally
//fail with RuntimeException

结语


本次的kotlin协程分享也结束了,内容篇基础,也算是对kotlin协程的一个入门。当对它的使用达到熟练时,会继续分享一篇关于较进阶的文章,希望大家喜欢。


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

另类年终总结:在煤老板开的软件公司实习是怎样一种体验?

某个编剧曾经说过:“怀念煤老板,他们从不干预我们创作,除了要求找女演员外,没有别的要求。”,现在的我毕业后正式工作快半年了,手上的活越来越多,负责的事项越来越多越来越杂,偶尔夜深人静走在回家的路上,也怀念当时在煤老板旗下的软件公司实习时无忧无虑的快乐生活,谨以...
继续阅读 »

某个编剧曾经说过:“怀念煤老板,他们从不干预我们创作,除了要求找女演员外,没有别的要求。”,现在的我毕业后正式工作快半年了,手上的活越来越多,负责的事项越来越多越来越杂,偶尔夜深人静走在回家的路上,也怀念当时在煤老板旗下的软件公司实习时无忧无虑的快乐生活,谨以此文纪念一下当时的时光。


煤老板还会开软件公司?


是的,煤老板家大业大,除了名下有几座矿之外,还有好多处农场、餐厅、物流等产业,可以说涉足了多个产业。当然最赚钱的主业还是矿业,听坊间传闻说,只要矿一开,钱就是哗哗的流进来。那么这个软件公司主要是做什么的呢,一小部分是给矿业服务的,负责矿山的相关人员使用记录展示每天矿上的相关数据,比如每天运输车辆的流转、每日矿上人力的核算。大部分的主力主要用于实现老板的雄伟理想,通过一个超级APP,搞定衣食住行,具体的业务如下,可以说是相当红火的。



煤老板的软件公司是怎么招聘的


这么有特色的一家公司,我是如何了解到并加入的呢。这还要从老板如何创立这家公司说起,老板在大学进修MBA的时候,认识了大学里计算机学院的几名优秀学子,然后对他们侃侃而谈自己的理念和对未来的设想,随后老板大笔一挥,我开家公司,咱们一起创业吧,钱我出,你们负责出技术。然后这几个计算机学院的同学,就携带着技术入股成为了这家软件公司的一员。随着老板的设想越来越丰富,最初进去的技术骨干也在不停的招兵买马,当时还是流行在QQ空间转发招聘信息。正是在茫茫动态中,多看了招聘信息一眼,使得该公司深深留在我的印象当中。后来我投递的时候,也是大学同学正在里面实习,于是简历直达主管。


面试都问了些啥


由于公司还处于初创阶段,所以没有那么复杂的一面二面三面HR面,一上来就是技术主管们来一个3对1面,开头聊聊大家都是校友,甚至可能还是同一个导师下的师兄弟,所以面试相对来说就没有那么难,问一问大学里写过的大作业项目,聊一聊之前实习做的东西,问一问熟悉的八股文,比如数据库事务,Spring等等,最后再关切的问一下实习时间,然后就送客等HR通知了。


工作都需要干啥


正如第一张图所示,公司的产品分成了几个模块,麻雀虽小,五脏俱全,公司里后端、前端、移动端、测试一应具全。我参与的正是公司智慧餐饮行业线的后端开发,俗称Java CRUD boy。由于公司里一众高薪招揽过来的开发,整体采用的开发理念还是很先进的。会使用sprint开发流程,每周一个迭代,就是发版上线还是不够devops,需要每周五技术leader自己启动各个脚本进行发版,将最新的代码启动到阿里云服务机器上。 虽然用户的体量不是很大,但是仍然包含Spring Cloud分布式框架、分库分表、Redis分布式锁、Elastic Search搜索框架、DTS消息传输复制框架等“高新科技”。每周伊始,会先进行需求评审,评估一下开发需要的工作量,随后就根据事先制定的节奏进行有条不紊的开发、测试、验收、上线。虽然工作难度不高,但是我在这家公司第一次亲身参与了产品迭代的全流程,为以后的实习、找工作都添加了一些工作经验。


因为是实习嘛,所以基本上都是踩点上班、准时下班。不过偶尔也存在老板一拍脑袋,说我们要两周造一个电子商城的情况,那个时候可真是加班加点,披星戴月带月的把项目的简易版本给完成、上线了。但是比较遗憾的是,后面也没有能大范围投入使用。


比如下面的自助借伞机,就是前司的一项业务,多少也是帮助了一些同学免于淋雨。



画重点,福利究竟有多好


首先公司的办公地点位于南京市中心,与新街口德基隔基相望。



每天发价值88元的内部币,用于在楼下老板开的餐厅里点餐,工作套餐有荤有素有汤有水果,可以说是非常的上流了。



如果不想吃工作套餐,还可以一起聚众点餐,一流的淮扬菜式,可以说非常爽了。 听说在点餐系统刚上线还没有内部币时,点餐是通过白名单的方式,不用付钱随便点。可惜我来晚了,没有体验到这么个好时候。



工作也标配imac一整套,虽然不好带走移动办公,但是用起来依然逼格满满。



熟悉的健身房福利当然少不了,而且还有波光粼粼的大泳池,后悔没有利用当时的机会多去几次学会游泳了。



除了这些基础福利之外,老板给的薪资比肩BAT大厂,甚至可能比他们还高一丢丢,在南京可以生活的相当滋润了。


既然说的这么好,那么为啥没有留下来呢。


唯一的问题当然是因为公司本身尚未盈利,所有这一切都依赖老板一个人的激情投入,假如老板这边出了啥问题,那整个公司也就将皮之不存,毛将焉附了。用软件领域的话来说,就是整个系统存在单点故障。所以尽管当时的各种福利很好,也选择离开找个更大的厂子先进去锻炼锻炼。


最后希望前老板矿上的生意越来越好,哪天我在外面卷不动了,还能收留我一下。


作者:日暮与星辰之间
来源:juejin.cn/post/7174065718386753543
收起阅读 »

告诉你为什么视频广告点不了关闭

前言我们平时玩游戏多多少少会碰到一些视频广告,看完后是能领取游戏奖励的,然后你会发现有时候看完点击那个关闭按钮,结果是跳下载,你理所当然的认为是点击到了外边,事实真的是这样的吗?有些东西不好那啥,你们懂的,所以以下内容纯属我个人猜测,纯属虚构1. 整个广告流程...
继续阅读 »

前言

我们平时玩游戏多多少少会碰到一些视频广告,看完后是能领取游戏奖励的,然后你会发现有时候看完点击那个关闭按钮,结果是跳下载,你理所当然的认为是点击到了外边,事实真的是这样的吗?有些东西不好那啥,你们懂的,所以以下内容纯属我个人猜测,纯属虚构

1. 整个广告流程的各个角色

要想对广告这东西有个大概的了解,你得先知道你看广告的过程中都有哪些角色参与了进来。

简单来说,是有三方参与了进来:
(1)广告提供商:顾名思义负责提供广告,比如你看的广告是一款游戏的广告,那这个游戏的公司就是广告的提供商。
(2)当前应用:就是播放这个广告的应用。
(3)平台:播放广告这个操作就是平台负责的,它负责连接上面两方,从广告提供商中拿到广告,然后让当前应用接入。

平台有很多,比如字节、腾讯都有相对应的广告平台,或者一些小公司自己做广告平台。他们之间的py交易是这样的:所有广告的功能是由平台去开发,然后他会提供一套sdk或者什么的让应用接入,应用你接入之后每播放1次广告,平台就给你多少钱,但是播放的是什么广告,这个就是平台自己去下发。然后广告提供商就找到平台,和他谈商业合作,你帮我展示我家的产品的广告多少次,我给你多少钱。 简单来说他们之间的交易就是这样。

简单来说,就是广告提供商想要影响力,其它两方要钱,他们都希望广告能更多的展示。

2. 广告提供商的操作

广告提供商是花钱让平台推广广告的,那我肯定是希望尽量每次广告的展示都有用户去点击然后下载我们家的应用。

所以广告提供商想出了一个很坏的办法,相信大家都遇到过,就是我播放视频,我在视频的最后几帧里面的图片的右上角放一个关闭图片,误导用户这个关闭图片是点了之后能关闭的,其实它是视频的一部分,所以你点了就相当于点了视频,那就触发跳转下载应用这些操作。

破解的方法也很简单,你等到计算结束后的几秒再点关闭按钮,不要一看到关闭按钮的图片出来马上点。

3. 应用的操作

应用是很难在广告播放的时候去做手脚,因为这部分的代码不是他们写的,他们只是去调用平台写的代码。

那他们想让广告尽可能多的展示,唯一能做的就是把展示广告的地方增加,尽可能多的让更多场景能展示广告。当然这也有副作用,你要是这个应用点哪里都是广告,这不得把用户给搞吐了,砸了自己的口碑,如果只有一些地方有,用户还是能理解的,毕竟赚钱嘛,不寒参。

4. 平台的操作

平台的操作那就丰富了,代码是我写的,兄弟,我想怎么玩就怎么玩,我能有一百种方法算计你。

猜测的,注意,是猜测的[狗头]

有的人说,故意把关闭按钮设置小,让我们误触关闭按钮以外的区域。我只能说,你让我来做,我都不屑于把关闭按钮设置小。

我们都知道平时开发时,我们觉得点击按钮不灵,所以我们想扩大图标的点击区域,但是又不想改变图标的大小,所以我们用padding来实现。同样的,我也能做到不改变图标的大小,然后缩小点击的范围

我写一个自定义view(假设就是关闭图标)

public class TestV extends View {

   public TestV(Context context) {
       super(context);
  }

   public TestV(Context context, AttributeSet attrs) {
       super(context, attrs);
  }

   public TestV(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
  }

   @Override
   public boolean dispatchTouchEvent(MotionEvent event) {
       if (event.getAction() == MotionEvent.ACTION_DOWN) {
           int w = getMeasuredWidth();
           int h = getMeasuredHeight();
           Log.d("mmp", "============ view点击");
           if (event.getX() < w / 4 || event.getX() > 3 * w / 4 || event.getY() < h / 4 || event.getY() > 3 * h / 4) {
               return super.dispatchTouchEvent(event);
          } else {
               Log.d("mmp", "============ view点击触发-》关闭");
               return true;
          }
      }
       return super.dispatchTouchEvent(event);
  }
}

代码很简单就不过多讲解,能看出我很简单就实现让点击范围缩小1/4。所以当你点到边缘的时候,其实就相当于点到了广告。

除了缩小范围之外,我还能设置2秒前后点击是不同的效果,你有没有一种感觉,第一次点关闭按钮就是跳到下载应用,然后返回再点击就是关闭,所以你觉得是你第一次点击的时候是误触了外边。

public class TestV extends View {

   private boolean canClose = true;

   public TestV(Context context) {
       super(context);
  }

   public TestV(Context context, AttributeSet attrs) {
       super(context, attrs);
  }

   public TestV(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
  }

   @Override
   public void setVisibility(int visibility) {
       super.setVisibility(visibility);
       if (visibility == View.VISIBLE) {
           canClose = false;
      }
  }

   @Override
   public boolean dispatchTouchEvent(MotionEvent event) {
       if (event.getAction() == MotionEvent.ACTION_DOWN) {
           int w = getMeasuredWidth();
           int h = getMeasuredHeight();
           Log.d("mmp", "============ view点击");
           if (!canClose) {
               return super.dispatchTouchEvent(event);
          } else {
               Log.d("mmp", "============ view点击触发-》关闭");
               return true;
          }
      }
       return super.dispatchTouchEvent(event);
  }

   // 播放完成
   public void playFinish() {
       setVisibility(VISIBLE);
       Handler handler = new Handler(Looper.getMainLooper());
       handler.postDelayed(new Runnable() {
           @Override
           public void run() {
               canClose = true;
          }
      }, 2000);
  }

}

播放完成之后调用playFinish方法,然后会把canClose这个状态设置为false,2秒后再设为true。这样你在2秒前点击按钮,其实就是点击外部的效果,也就会跳去下载。

而且你注意,这些限制,我可以不写死在代码里面,可以用后台返回,比如这个2000,我可以后台返回。我就能做到比如第一天我返回0,你觉得没什么问题,能正常点关闭,我判断你是第二天,我直接返2000给你,然后你一想,之前都是正常的,那这次不正常,肯定是我点错。

你以为的意外只不过是我想让你以为是意外罢了。那这个如何去破解呢?我只能说无解,他能有100种方法让你点中跳出去下载,那还能有是什么解法?

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

收起阅读 »

Android App Bundle

1. Android App Bundle 是什么?从 2021 年 8 月起,新应用需要使用 Android App Bundle 才能在 Google Play 中发布。Android App Bundle是一种发布格式,打包出来的格式为aab,而之前我们...
继续阅读 »

1. Android App Bundle 是什么?

从 2021 年 8 月起,新应用需要使用 Android App Bundle 才能在 Google Play 中发布。

Android App Bundle是一种发布格式,打包出来的格式为aab,而之前我们打包出来的格式为apk。编写完代码之后,将其打包成aab格式(里面包含了所有经过编译的代码和资源),然后上传到Google Play。用户最后安装的还是apk,只不过不是一个,而是多个apk,这些apk是Google Play根据App Bundle生成的。

既然已经有了apk,那要App Bundle有啥用?咱之前打一个apk,会把各种架构、各种语言、各种分辨率的图片等全部放入一个apk中,但具体到某个用户的设备上,这个设备只需要一种so库架构、一种语言、一种分辨率的图片,那其他的东西都在apk里面,这就有点浪费了,不仅下载需要更多的流量,而且还占用用户设备更多的存储空间。当然,也可以通过在打包的时候打多个apk,分别支持各种密度、架构、语言的设备,但这太麻烦了。

于是,Google Play出手了。

App Bundle是经过签名的二进制文件,可将应用的代码和资源组织到不同的模块中。比如,当某个用户的设备是xxhdpi+arm64-v8a+values-zh环境,那Google Play后台会利用App Bundle中的对应的模块(xxhdpi+arm64-v8a+values-zh)组装起来,组成一个base apk和多个配置apk供该用户下载并安装,而不会去把其他的像armeabi-v7ax86之类的与当前设备无关的东西组装进apk,这样用户下载的apk体积就会小很多。体积越小,转化率越高,也更环保。

有了Android App Bundle之后,Google Play还提供了2个东西:Play Feature DeliveryPlay Asset Delivery。Play Feature Delivery可以按某种条件分发或按需下载应用的某些功能,从而进一步减小包体积。Play Asset Delivery是Google Play用于分发大体积应用的解决方案,为开发者提供了灵活的分发方式和极高的性能。

2. Android App Bundle打包

打Android App Bundle非常简单,直接通过Android Studio就能很方便地打包,当然命令行也可以的。

  • Android Studio打包:Build -> Generate Signed Bundle / APK -> 选中Android App Bundle -> 选中签名和输入密码 -> 选中debug或者release包 -> finish开始打包

  • gradle命令行打包:./gradlew bundleDebug 或者 ./gradlew bundleRelease

打出来之后是一个类似app-debug.aab的文件,可以将aab文件直接拖入Android Studio进行分析和查看其内部结构,很方便。

3. 如何测试Android App Bundle?

Android App Bundle包倒是打出来了,那怎么进行测试呢?我们设备上仅允许安装apk文件,aab是不能直接进行安装的。这里官方提供了3种方式可供选择:Android Studio 、Google Play 和 bundletool,下面我们一一来介绍。

3.1 Android Studio

利用Android Studio,在我们平时开发时就可以直接将项目打包成debug的aab并且运行到设备上,只需要点一下运行按钮即可(当然,这之前需要一些简单的配置才行)。Android Studio和Google Play使用相同的工具从aab中提取apk并将其安装在设备上,因此这种本地测试策略也是可行的。这种方式可以验证以下几点:

  • 该项目是否可以构建为app bundle

  • Android Studio是否能够从app bundle中提取目标设备配置的apk

  • 功能模块的功能与应用的基本模块是否兼容

  • 该项目是否可以在目标设备上按预期运行

默认情况下,设备连接上Android Studio之后,运行时打的包是apk。所以我们需要配置一下,改成运行时先打app bundle,然后再从app bundle中提取出该设备需要的配置apk,再组装成一个新的apk并签名,随后安装到设备上。具体配置步骤如下:

  1. 从菜单栏中依次选择 Run -> Edit Configurations。

  2. 从左侧窗格中选择一项运行/调试配置。

  3. 在右侧窗格中,选择 General 标签页。

  4. 从 Deploy 旁边的下拉菜单中选择 APK from app bundle。

  5. 如果你的应用包含要测试的免安装应用体验,请选中 Deploy as an instant app 旁边的复选框。

  6. 如果你的应用包含功能模块,你可以通过选中每个模块旁边的复选框来选择要部署的模块。默认情况下,Android Studio 会部署所有功能模块,并且始终都会部署基本应用模块。

  7. 点击 Apply 或 OK。

好了,现在已经配置好了,现在点击运行按钮,Android Studio会构建app bundle,并使用它来仅部署连接的设备及你选择的功能模块所需要的apk。

3.2 bundletool

bundletool 是一种命令行工具,谷歌开源的,Android Studio、Android Gradle 插件和 Google Play 使用这一工具将应用的经过编译的代码和资源转换为 App Bundle,并根据这些 Bundle 生成可部署的 APK。

前面使用Android Studio来测试app bundle比较方便,但是,官方推荐使用bundletool 从 app bundle 将应用部署到连接的设备。因为bundletool提供了专门为了帮助你测试app bundle并模拟通过Google Play分发而设计的命令,这样的话我们就不必上传到Google Play管理中心去测试了。

下面我们就来实验一把。

  1. 首先是下载bundletool,到GitHub上去下载bundletool,地址:github.com/google/bund…

  2. 然后通过Android Studio或者Gradle将项目打包成Android App Bundle,然后通过bundletool将Android App Bundle生成一个apk容器(官方称之为split APKs),这个容器以.apks作为文件扩展名,这个容器里面包含了该应用支持的所有设备配置的一组apk。这么说可能不太好懂,我们实操一下:

//使用debug签名生成apk容器
java -jar bundletool-all-1.14.0.jar build-apks --bundle=app-release.aab --output=my_app.apks

//使用自己的签名生成apk容器
java -jar bundletool-all-1.14.0.jar build-apks --bundle=app-release.aab --output=my_app.apks
--ks=keystore.jks
--ks-pass=file:keystore.pwd
--ks-key-alias=MyKeyAlias
--key-pass=file:key.pwd

ps: build-apks命令是用来打apks容器的,它有很多可选参数,比如这里的--bundle=path表示:指定你的 app bundle 的路径,--output=path表示:指定输出 .apks 文件的名称,该文件中包含了应用的所有 APK 零部件。它的其他参数大家感兴趣可以到bundletool查阅。

执行完命令之后,会生成一个my_app.apks的文件,我们可以把这个apks文件解压出来,看看里面有什么。

 toc.pb

└─splits
       base-af.apk
       base-am.apk
       base-ar.apk
       base-as.apk
       base-az.apk
       base-be.apk
       base-bg.apk
       base-bn.apk
       base-bs.apk
       base-ca.apk
       base-cs.apk
       base-da.apk
       base-de.apk
       base-el.apk
       base-en.apk
       base-es.apk
       base-et.apk
       base-eu.apk
       base-fa.apk
       base-fi.apk
       base-fr.apk
       base-gl.apk
       base-gu.apk
       base-hdpi.apk
       base-hi.apk
       base-hr.apk
       base-hu.apk
       base-hy.apk
       base-in.apk
       base-is.apk
       base-it.apk
       base-iw.apk
       base-ja.apk
       base-ka.apk
       base-kk.apk
       base-km.apk
       base-kn.apk
       base-ko.apk
       base-ky.apk
       base-ldpi.apk
       base-lo.apk
       base-lt.apk
       base-lv.apk
       base-master.apk
       base-mdpi.apk
       base-mk.apk
       base-ml.apk
       base-mn.apk
       base-mr.apk
       base-ms.apk
       base-my.apk
       base-nb.apk
       base-ne.apk
       base-nl.apk
       base-or.apk
       base-pa.apk
       base-pl.apk
       base-pt.apk
       base-ro.apk
       base-ru.apk
       base-si.apk
       base-sk.apk
       base-sl.apk
       base-sq.apk
       base-sr.apk
       base-sv.apk
       base-sw.apk
       base-ta.apk
       base-te.apk
       base-th.apk
       base-tl.apk
       base-tr.apk
       base-tvdpi.apk
       base-uk.apk
       base-ur.apk
       base-uz.apk
       base-vi.apk
       base-xhdpi.apk
       base-xxhdpi.apk
       base-xxxhdpi.apk
       base-zh.apk
       base-zu.apk

里面有一个toc.pb文件和一个splits文件夹(splits顾名思义,就是拆分出来的所有apk文件),splits里面有很多apk,base-开头的apk是主module的相关apk,其中base-master.apk是基本功能apk,base-xxhdpi.apk则是对资源分辨率进行了拆分,base-zh.apk则是对语言资源进行拆分。

我们可以将这些apk拖入Android Studio看一下里面有什么,比如base-xxhdpi.apk

│  AndroidManifest.xml
|  
| resources.arsc

├─META-INF
│     BNDLTOOL.RSA
│     BNDLTOOL.SF
│     MANIFEST.MF

└─res
   ├─drawable-ldrtl-xxhdpi-v17
   │     abc_ic_menu_copy_mtrl_am_alpha.png
   │     abc_ic_menu_cut_mtrl_alpha.png
   │     abc_spinner_mtrl_am_alpha.9.png
   
   ├─drawable-xhdpi-v4
   │     notification_bg_low_normal.9.png
   │     notification_bg_low_pressed.9.png
   │     notification_bg_normal.9.png
   │     notification_bg_normal_pressed.9.png
   │     notify_panel_notification_icon_bg.png
   
   └─drawable-xxhdpi-v4
           abc_textfield_default_mtrl_alpha.9.png
           abc_textfield_search_activated_mtrl_alpha.9.png
           abc_textfield_search_default_mtrl_alpha.9.png
           abc_text_select_handle_left_mtrl_dark.png
           abc_text_select_handle_left_mtrl_light.png
           abc_text_select_handle_middle_mtrl_dark.png
           abc_text_select_handle_middle_mtrl_light.png
           abc_text_select_handle_right_mtrl_dark.png
           abc_text_select_handle_right_mtrl_light.png

首先,这个apk有自己的AndroidManifest.xml,其次是resources.arsc,还有META-INF签名信息,最后是与自己名称对应的xxhdpi的资源。

再来看一个base-zh.apk:

│  AndroidManifest.xml
│ resources.arsc

└─META-INF
       BNDLTOOL.RSA
       BNDLTOOL.SF
       MANIFEST.MF

也是有自己的AndroidManifest.xml、resources.arsc、签名信息,其中resources.arsc里面包含了字符串资源(可以直接在Android Studio中查看)。

分析到这里大家对apks文件就有一定的了解了,它是一个压缩文件,里面包含了各种最终需要组成apk的各种零部件,这些零部件可以根据设备来按需组成一个完整的app。 比如我有一个设备是只支持中文、xxhdpi分辨率的设备,那么这个设备其实只需要下载部分apk就行了,也就是base-master.apk(基本功能的apk)、base-zh.apk(中文语言资源)和base-xxhdpi.apk(图片资源)给组合起来。到Google Play上下载apk,也是这个流程(如果这个项目的后台上传的是app bundle的话),Google Play会根据设备的特性(CPU架构、语言、分辨率等),首先下载基本功能apk,然后下载与之配置的CPU架构的apk、语言apk、分辨率apk等,这样下载的apk是最小的。

  1. 生成好了apks之后,现在我们可以把安卓测试设备插上电脑,然后利用bundletool将apks中适合设备的零部件apk挑选出来,并部署到已连接的测试设备。具体操作命令:java -jar bundletool-all-1.14.0.jar install-apks --apks=my_app.apks,执行完该命令之后设备上就安装好app了,可以对app进行测试了。bundletool会去识别这个测试设备的语言、分辨率、CPU架构等,然后挑选合适的apk安装到设备上,base-master.apk是首先需要安装的,其次是语言、分辨率、CPU架构之类的apk,利用Android 5.0以上的split apks,这些apk安装之后可以共享一套代码和资源。

3.3 Google Play

如果我最终就是要将Android App Bundle发布到Google Play,那可以先上传到Google Play Console的测试渠道,再通过测试渠道进行分发,然后到Google Play下载这个测试的App,这样肯定是最贴近于用户的使用环境的,比较推荐这种方式进行最后的测试。

4. 拆解Android App Bundle格式

首先,放上官方的格式拆解图(下图包含:一个基本模块、两个功能模块、两个资源包):


app bundle是经过签名的二进制文件,可将应用的代码和资源装进不同的模块中,这些模块中的代码和资源的组织方式和apk中相似,它们都可以作为单独的apk生成。Google Play会使用app bundle生成向用户提供的各种apk,如base apk、feature apk、configuration apks、multi-APKs。图中蓝色标识的目录(drawable、values、lib)表示Google Play用来为每个模块创建configuration apks的代码和资源。

  • base、feature1、feature2:每个顶级目录都表示一个不同的应用模块,基本模块是包含在app bundle的base目录中。

  • asset_pack_1asset_pack_2:游戏或者大型应用如果需要大量图片,则可以将asset模块化处理成资源包。资源包可以根据自己的需要,在合适的时机去请求到本地来。

  • BUNDLE-METADATA/:包含元数据文件,其中包含对工具或应用商店有用的信息。

  • 模块协议缓冲区(*pb)文件:元数据文件,向应用商店说明每个模块的内容。如:BundleConfig.pb 提供了有关 bundle 本身的信息(如用于构建 app bundle 的构建工具版本),native.pb 和 resources.pb 说明了每个模块中的代码和资源,这在 Google Play 针对不同的设备配置优化 APK 时非常有用。

  • manifest/:与 APK 不同,app bundle 将每个模块的 AndroidManifest.xml 文件存储在这个单独的目录中。

  • dex/:与 APK 不同,app bundle 将每个模块的 DEX 文件存储在这个单独的目录中。

  • res/lib/assets/:这些目录与典型 APK 中的目录完全相同。

  • root/:此目录存储的文件之后会重新定位到包含此目录所在模块的任意 APK 的根目录。

5. Split APKs

Android 5.0 及以上支持Split APKs机制,Split APKs与常规的apk相差不大,都是包含经过编译的dex字节码、资源和清单文件等。区别是:Android可以将安装的多个Split APKs视为一个应用,也就是虽然我安装了多个apk,但Android系统认为它们是同一个app,用户也只会在设置里面看到一个app被安装上了;而平时我们安装的普通apk,一个apk就对应着一个app。Android上,我们可以安装多个Split APK,它们是共用代码和资源的。

Split APKs的好处是可以将单体式app做拆分,比如将ABI、屏幕密度、语言等形式拆分成多个独立的apk,按需下载和安装,这样可以让用户更快的下载并安装好apk,并且占用更小的空间。

Android App Bundle最终也就是利用这种方式来进行安装的,比如我上面在执行完java -jar bundletool-all-1.14.0.jar install-apks --apks=my_app.apks命令之后,那么最后安装到手机上的apk文件如下:


ps:5.0以下不支持Split APKs,那咋办?没事,Google Play会为这些设备的用户安装一个全量的apk,里面什么都有,问题不大。

6. 国内商店支持Android App Bundle吗?

Android App Bundle不是Google Play的专有格式,它是开源的,任何商店想支持都可以的。

上面扯那么大一堆有的没的,这玩意儿这么好用,那国内商店的支持情况如何。我查了下,发现就华为可以支持,手动狗头。

华为 Android App Bundle developer.huawei.com/consumer/cn…

7. 小结

现在上架Google Play必须上传Android App Bundle才行了,所以有必要简单了解下。简单来说就是Android App Bundle是一种新的发布格式,上传到商店之后,商店会利用这个Android App Bundle生成一堆Split APKs,当用户要去安装某个app时,只需要按需下载Split APKs中的部分apk(base apk + 各种配置apk),进行安装即可,总下载量大大减少。

参考资料

作者:潇风寒月
来源:juejin.cn/post/7197246543207022629

收起阅读 »

android 微信抢红包工具 AccessibilityService(上)

一、目标二、实现流程我们把一个抢红包发的过程拆分来看,可以分为几个步骤:以上是一个抢红包的基本流程。1、收到通知 以及 点击通知栏Ⅰ、AccessibilityServiceⅡ、NotificationListenerService2、点击红包我们来分析一下,...
继续阅读 »

你有因为手速不够快抢不到红包而沮丧? 你有因为错过红包而懊恼吗? 没错,它来了。。。

一、目标

使用AccessibilityService的方式,实现微信自动抢红包(吐槽一下,网上找了许多文档,由于各种原因,无法实现对应效果,所以先给自己整理下),关于AccessibilityService的文章,网上有很多(没错,多的都懒得贴链接那种多),可自行查找。

二、实现流程

1、流程分析(这里只分析在桌面的情况)

我们把一个抢红包发的过程拆分来看,可以分为几个步骤:

收到通知 -> 点击通知栏 -> 点击红包 -> 点击开红包 -> 退出红包详情页

以上是一个抢红包的基本流程。

2、实现步骤

1、收到通知 以及 点击通知栏

接收通知栏的消息,介绍两种方式

Ⅰ、AccessibilityService

即通过AccessibilityService的AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED事件来获取到Notification

private fun handleNotification(eventAccessibilityEvent) {
   val texts = event.text
   if (!texts.isEmpty()) {
           for (text in texts) {
               val content = text.toString()
               //如果微信红包的提示信息,则模拟点击进入相应的聊天窗口
               if (content.contains("[微信红包]")) {
                   if (event.parcelableData != null && event.parcelableData is Notification) {
                       val notificationNotification? = event.parcelableData as Notification?
                       val pendingIntentPendingIntent = notification!!.contentIntent
                       try {
                           pendingIntent.send()
                      } catch (eCanceledException) {
                           e.printStackTrace()
                      }
                  }
              }
          }
      }

}
Ⅱ、NotificationListenerService

这是监听通知栏的另一种方式,记得要获取权限哦

class MyNotificationListenerService : NotificationListenerService() {

   override fun onNotificationPosted(sbnStatusBarNotification?) {
       super.onNotificationPosted(sbn)

       val extras = sbn?.notification?.extras
       // 获取接收消息APP的包名
       val notificationPkg = sbn?.packageName
       // 获取接收消息的抬头
       val notificationTitle = extras?.getString(Notification.EXTRA_TITLE)
       // 获取接收消息的内容
       val notificationText = extras?.getString(Notification.EXTRA_TEXT)
       if (notificationPkg != null) {
           Log.d("收到的消息内容包名:"notificationPkg)
           if (notificationPkg == "com.tencent.mm"){
               if (notificationText?.contains("[微信红包]"== true){
                   //收到微信红包了
                   val intent = sbn.notification.contentIntent
                   intent.send()
              }
          }
      }
       Log.d("收到的消息内容""Notification posted $notificationTitle & $notificationText")
  }

   override fun onNotificationRemoved(sbnStatusBarNotification?) {
       super.onNotificationRemoved(sbn)
  }
}

2、点击红包

通过上述的跳转,可以进入聊天详情页面,到达详情页之后,接下来就是点击对应的红包卡片,那么问题来了,怎么点?肯定不是手动点。。。

我们来分析一下,一个聊天列表中,我们怎样才能识别到红包卡片,我看网上有通过findAccessibilityNodeInfosByViewId来获取对应的View,这个也可以,只是我们获取id的方式需要借助工具,可以用Android Device Monitor,但是这玩意早就废废弃了,虽然在sdk的目录下存在monitor,奈何本人太菜,点击就是打不开


我本地的jdk是11,我怀疑是不兼容,毕竟Android Device Monitor太老了。换新的layout Inspector,也就看看本地的debug应用,无法查看微信的呀。要么就反编译,这个就先不考虑了,或者在配置文件中设置android:accessibilityFlags="flagReportViewIds",然后暴力遍历Node树,打印相应的viewId和className,找到目标id即可。当然也可以换findAccessibilityNodeInfosByText这个方法试试。

这个方法从字面意思能看出来,是通过text来匹配的,我们可以知道红包卡片上面是有“微信红包”的固定字样的,是不是可以通股票这个来匹配呢,这还有个其他问题,并不是所有的红包都需要点,比如已过期,已领取的是不是要过滤下,咋一看挺好过滤的,一个循环就好,仔细想,这是棵树,不太好剔除,所以换了个思路。

最终方案就是递归一棵树,往一个列表里面塞值,“已过期”和“已领取”的塞一个字符串“#”,匹配到“微信红包”的塞一个AccessibilityNodeInfo,这样如果这个红包不能抢,那肯定一前一后分别是一个字符串和一个AccessibilityNodeInfo,因此,我们读到一个AccessibilityNodeInfo,并且前一个值不是字符串,就可以执行点击事件,代码如下

private fun getPacket() {
   val rootNode = rootInActiveWindow
   val caches:ArrayList<Any> = ArrayList()
   recycle(rootNode,caches)
   if(caches.isNotEmpty()){
       for(index in 0 until caches.size){
           if(caches[indexis AccessibilityNodeInfo && (index == 0 || caches[index-1!is String )){
               val node = caches[indexas AccessibilityNodeInfo
               var parent = node.parent
               while (parent != null) {
                   if (parent.isClickable) {
                       parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                       break
                  }
                   parent = parent.parent
              }
               break
          }
      }
  }

}

private fun recycle(nodeAccessibilityNodeInfo,caches:ArrayList<Any>) {
       if (node.childCount == 0) {
           if (node.text != null) {
               if ("已过期" == node.text.toString() || "已被领完" == node.text.toString() || "已领取" == node.text.toString()) {
                   caches.add("#")
              }

               if ("微信红包" == node.text.toString()) {
                   caches.add(node)
              }
          }
      } else {
           for (i in 0 until node.childCount) {
               if (node.getChild(i!= null) {
                   recycle(node.getChild(i),caches)
              }
          }
      }
  }

以上只点击了第一个能点击的红包卡片,想点击所有的可另行处理。

3、点击开红包

这里思路跟上面类似,开红包页面比较简单,但是奈何开红包是个按钮,在不知道id的前提下,我们也不知道则呢么获取它,所以采用迂回套路,找固定的东西,我这里发现每个开红包的页面都有个“xxx的红包”文案,然后这个页面比较简单,只有个关闭,和开红包,我们通过获取“xxx的红包”对应的View来获取父View,然后递归子View,判断可点击的,执行点击事件不就可以了吗

private fun openPacket() {
   val nodeInfo = rootInActiveWindow
   if (nodeInfo != null) {
       val list = nodeInfo.findAccessibilityNodeInfosByText ("的红包")
       for ( i in 0 until list.size) {
           val parent = list[i].parent
           if (parent != null) {
               for ( j in 0 until  parent.childCount) {
                   val child = parent.getChild (j)
                   if (child != null && child.isClickable) {
                       child.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                  }
              }

          }

      }
  }

}

4、退出红包详情页

这里回退也是个按钮,我们也不知道id,所以可以跟点开红包一样,迂回套路,获取其他的View,来获取父布局,然后递归子布局,依次执行点击事件,当然关闭事件是在前面的,也就是说关闭会优先执行到

private fun close() {
   val nodeInfo = rootInActiveWindow
   if (nodeInfo != null) {
       val list = nodeInfo.findAccessibilityNodeInfosByText ("的红包")
       if (list.isNotEmpty()) {
           val parent = list[0].parent.parent.parent
           if (parent != null) {
               for ( j in 0 until  parent.childCount) {
                   val child = parent.getChild (j)
                   if (child != null && child.isClickable) {
                       child.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                  }
              }

          }

      }
  }
}

三、遇到问题

1、AccessibilityService收不到AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED事件

android碎片问题很正常,我这边是使用NotificationListenerService来替代的。

2、需要点击View的定位

简单是就是到页面应该点哪个View,找到相应的规则,来过滤出对应的View,这个规则是随着微信的改变而变化的,findAccessibilityNodeInfosByViewId最直接,但是奈何工具问题,有点麻烦,遍历打印可以获取,但是id每个版本可能会变。还有就是通过文案来获取,即findAccessibilityNodeInfosByText,获取一些固定文案的View,这个相对而言在不改版,可能不会变,相对稳定些,如果这个文案的View本身没点击事件,可获取它的parent,尝试点击,或者遍历parent树,根据isClickable来判断是否可以点击。

划重点:

这里还有一种就是钉钉的开红包按钮,折腾了半天,始终拿不到,各种递归遍历,一直没有找到,最后换了个方式,通过AccessibilityService的模拟点击来做,也就是通过坐标来模拟点击,当然要在配置中开启android:canPerformGestures="true", 然后通过 accessibilityService.dispatchGesture() 来处理,具体坐标可以拿一个其他的View,然后通过比例来确定大概得位置,或者,看看能不能拿到外层的Layout也是一样的

object AccessibilityClick {
   fun click(accessibilityServiceAccessibilityServicexFloatyFloat) {
       val builder = GestureDescription.Builder()
       val path = Path()
       path.moveTo(xy)
       path.lineTo(xy)
       builder.addStroke(GestureDescription.StrokeDescription(path010))
       accessibilityService.dispatchGesture(builder.build(), object : AccessibilityService.GestureResultCallback() {
           override fun onCancelled(gestureDescriptionGestureDescription) {
               super.onCancelled(gestureDescription)
          }

           override fun onCompleted(gestureDescriptionGestureDescription) {
               super.onCompleted(gestureDescription)
          }
      }, null)
  }
}

续:android 微信抢红包工具 AccessibilityService(下)

作者:我有一头小毛驴你有吗
来源:juejin.cn/post/7196949524061339703

收起阅读 »

android 微信抢红包工具 AccessibilityService(下)

接:android 微信抢红包工具 AccessibilityService(上)MyNotificationListenerServiceclass MyNotificationListenerService : Notific...
继续阅读 »

接:android 微信抢红包工具 AccessibilityService(上)

四、完整代码

MyNotificationListenerService

class MyNotificationListenerService : NotificationListenerService() {

   override fun onNotificationPosted(sbnStatusBarNotification?) {
       super.onNotificationPosted(sbn)

       val extras = sbn?.notification?.extras
       // 获取接收消息APP的包名
       val notificationPkg = sbn?.packageName
       // 获取接收消息的抬头
       val notificationTitle = extras?.getString(Notification.EXTRA_TITLE)
       // 获取接收消息的内容
       val notificationText = extras?.getString(Notification.EXTRA_TEXT)
       if (notificationPkg != null) {
           Log.d("收到的消息内容包名:"notificationPkg)
           if (notificationPkg == "com.tencent.mm"){
               if (notificationText?.contains("[微信红包]"== true){
                   //收到微信红包了
                   val intent = sbn.notification.contentIntent
                   intent.send()
              }
          }
      } Log.d("收到的消息内容""Notification posted $notificationTitle & $notificationText")
  }

   override fun onNotificationRemoved(sbnStatusBarNotification?) {
       super.onNotificationRemoved(sbn)
  }
}

MyAccessibilityService

class MyAccessibilityService : AccessibilityService() {

   override fun onAccessibilityEvent(eventAccessibilityEvent) {
       val eventType = event.eventType
       when (eventType) {
           AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> handleNotification(event)
           AccessibilityEvent.TYPE_WINDOW_STATE_CHANGEDAccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
               val className = event.className.toString()
               Log.e("测试无障碍",className)
               when (className) {
                   "com.tencent.mm.ui.LauncherUI" -> {
                       // 我管这叫红包卡片页面
                       getPacket()
                  }
                   "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI" -> {
                       // 貌似是老UI debug没发现进来
                       openPacket()
                  }
                   "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI" -> {
                       // 应该是红包弹框UI新页面 debug进来了
                       openPacket()
                  }
                   "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI" -> {
                       // 红包详情页面 执行关闭操作
                       close()
                  }
                   "androidx.recyclerview.widget.RecyclerView" -> {
                       // 这个比较频繁 主要是在聊天页面 有红包来的时候 会触发 当然其他有列表的页面也可能触发 没想到好的过滤方式
                       getPacket()
                  }
              }
          }
      }
  }

   /**
    * 处理通知栏信息
    *
    * 如果是微信红包的提示信息,则模拟点击
    *
    * @param event
    */
   private fun handleNotification(eventAccessibilityEvent) {
       val texts = event.text
       if (!texts.isEmpty()) {
           for (text in texts) {
               val content = text.toString()
               //如果微信红包的提示信息,则模拟点击进入相应的聊天窗口
               if (content.contains("[微信红包]")) {
                   if (event.parcelableData != null && event.parcelableData is Notification) {
                       val notificationNotification? = event.parcelableData as Notification?
                       val pendingIntentPendingIntent = notification!!.contentIntent
                       try {
                           pendingIntent.send()
                      } catch (eCanceledException) {
                           e.printStackTrace()
                      }
                  }
              }
          }
      }

  }

   /**
    * 关闭红包详情界面,实现自动返回聊天窗口
    */
   @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
   private fun close() {
       val nodeInfo = rootInActiveWindow
       if (nodeInfo != null) {
           val list = nodeInfo.findAccessibilityNodeInfosByText ("的红包")
           if (list.isNotEmpty()) {
               val parent = list[0].parent.parent.parent
               if (parent != null) {
                   for ( j in 0 until  parent.childCount) {
                       val child = parent.getChild (j)
                       if (child != null && child.isClickable) {
                           child.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                      }
                  }

              }

          }
      }
  }

   /**
    * 模拟点击,拆开红包
    */
   @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
   private fun openPacket() {
       Log.e("测试无障碍","点击红包")
       Thread.sleep(100)
       val nodeInfo = rootInActiveWindow
       if (nodeInfo != null) {
           val list = nodeInfo.findAccessibilityNodeInfosByText ("的红包")
           for ( i in 0 until list.size) {
               val parent = list[i].parent
               if (parent != null) {
                   for ( j in 0 until  parent.childCount) {
                       val child = parent.getChild (j)
                       if (child != null && child.isClickable) {
                           Log.e("测试无障碍","点击红包成功")
                           child.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                      }
              }

          }

          }
      }

  }

   /**
    * 模拟点击,打开抢红包界面
    */
   @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
   private fun getPacket() {
       Log.e("测试无障碍","获取红包")
       val rootNode = rootInActiveWindow
       val caches:ArrayList<Any> = ArrayList()
       recycle(rootNode,caches)
       if(caches.isNotEmpty()){
           for(index in 0 until caches.size){
               if(caches[indexis AccessibilityNodeInfo && (index == 0 || caches[index-1!is String )){
                   val node = caches[indexas AccessibilityNodeInfo
//                   node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                   var parent = node.parent
                   while (parent != null) {
                       if (parent.isClickable) {
                           parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                           Log.e("测试无障碍","获取红包成功")
                           break
                      }
                       parent = parent.parent
                  }
                   break
              }
          }
      }

  }

   /**
    * 递归查找当前聊天窗口中的红包信息
    *
    * 聊天窗口中的红包都存在"微信红包"一词,因此可根据该词查找红包
    *
    * @param node
    */
   private fun recycle(nodeAccessibilityNodeInfo,caches:ArrayList<Any>) {
       if (node.childCount == 0) {
           if (node.text != null) {
               if ("已过期" == node.text.toString() || "已被领完" == node.text.toString() || "已领取" == node.text.toString()) {
                   caches.add("#")
              }

               if ("微信红包" == node.text.toString()) {
                   caches.add(node)
              }
          }
      } else {
           for (i in 0 until node.childCount) {
               if (node.getChild(i!= null) {
                   recycle(node.getChild(i),caches)
              }
          }
      }
  }

   override fun onInterrupt() {}
   override fun onServiceConnected() {
       super.onServiceConnected()
       Log.e("测试无障碍id","启动")
       val infoAccessibilityServiceInfo = serviceInfo
       info.packageNames = arrayOf("com.tencent.mm")
       serviceInfo = info
  }
}

5、总结

此文是对AccessibilityService的使用的一个梳理,这个功能其实不麻烦,主要是一些细节问题,像自动领取支付宝红包,自动领取QQ红包或者其他功能等也都可以用类似方法实现。目前实现了微信和钉钉的,剩下的支付宝QQ啥的没啥人用,就不想做了,不过原理都是一样的,

源码地址: gitee.com/wlr123/acce…

使用时记得开启下对应权限,设置下后台运行权限,电量设置里面允许后台运行等,以及通知栏权限,以保证稳定运行


作者:我有一头小毛驴你有吗
来源:juejin.cn/post/7196949524061339703

收起阅读 »

从 微信 JS-SDK 认识 JSBridge

web
前言前段时间由于要实现 H5 移动端拉取微信卡包并同步卡包数据的功能,于是在项目中引入了 微信 JS-SDK(jweixin) 相关包实现功能,但也由此让我对其产生了好奇心,于是打算好好了解下相关的内容,通过查阅相关资料发现这其实属于&nb...
继续阅读 »

前言

前段时间由于要实现 H5 移动端拉取微信卡包并同步卡包数据的功能,于是在项目中引入了 微信 JS-SDK(jweixin) 相关包实现功能,但也由此让我对其产生了好奇心,于是打算好好了解下相关的内容,通过查阅相关资料发现这其实属于 JSBridge 的一种实现方式。

因此,只要了解 JSBridge 就能明白 微信 JS-SDK 是怎么一回事。


为什么需要 JSBridge?

相信大多数人都有相同的经历,第一次了解到关于 JSBridge 都是从 微信 JS-SDK(WeiXinJSBridge) 开始,当然如果你从事的是 Hybrid 应用 或 React-Native 开发的话相信你自然(应该、会)很了解。

其实 JSBridge 早就出现并被实际应用了,如早前桌面应用的消息推送等,而在移动端盛行的时代已经越来越需要 JSBridge,因为我们期望移动端(Hybrid 应用 或 React-Native)能做更多的事情,其中包括使用 客户端原生功能 提供更好的 交互 和 服务 等。

然而 JavaScript 并不能直接调用和它不同语言(如 Java、C/C++ 等)提供的功能特性,因此需要一个中间层去实现 JavaScript 与 其他语言 间的一个相互协作,这里通过一个 Node 架构来进行说明。

Node 架构


核心内容如下:

  • 顶层 Node Api

    • 提供 http 模块、流模块、fs文件模块等等,可以通过 JavaScript 直接调用
  • 中间层 Node Bindings

    • 主要是使 JavaScript 和 C/C++ 进行通信,原因是 JavaScript 无法直接调用 C/C++ 的库(libuv),需要一个中间的桥梁,node 中提供了很多 binding,这些称为 Node bindings
  • 底层 V8 + libuv

    • v8 负责解释、执行顶层的 JavaScript 代码
    • libuv 负责提供 I/O 相关的操作,其主要语言是 C/C++ 语言,其目的就是实现一个 跨平台(如 Windows、Linux 等)的异步 I/O 库,它直接与操作系统进行交互

这里不难发现 Node Bindings 就有点类似 JSBridge 的功能,所以 JSBridge 本身是一个很简单的东西,其更多的是 一种形式、一种思想

为什么叫 JSBridge?

Stack Overflow 联合创始人 Jeff Atwood 在 2007 年的博客《The Principle of Least Power》中认为 “任何可以使用 JavaScript 来编写的应用,并最终也会由 JavaScript 编写”,后来 JavaScript 的发展确实非常惊人,现在我们可以基于 JavaScript 来做各种事情,比如 网页、APP、小程序、后端等,并且各种相关的生态越来越丰富。

作为 Web 技术逻辑核心的 JavaScript 自然而然就需要承担与 其他技术 进行『桥接』的职责,而且任何一个 移动操作系统 中都会包含 运行 JavaScript 的容器环境,例如 WebViewJSCore 等,这就意味着 运行 JavaScript 不用像运行其他语言时需要额外添加相应的运行环境。

JSBridge 应用在国内真正流行起来则是因为 微信 的出现,当时微信的一个主要功能就是可以在网页中通过 JSBridge 来实现 内容分享

JSBridge 能做什么?

举个最常见的前端和后端的例子,后端只提供了一个查找接口,但是没有提供更新接口,那么对于前端来讲就是再想实现更新接口,也是没有任何法子的!

同样的,JSBridge 能做什么得看原生端给 JavaScript 提供调用 Native 什么功能的接口,比如通过 微信 JS-SDK 网页开发者可借助微信使用 拍照、选图、语音、位置 等手机系统的能力,同时可以直接使用 微信分享、扫一扫、卡券、支付 等微信特有的能力。

JSBridge 作为 JavaScript 与 Native 之间的一个 桥梁,表面上看是允许 JavaScript 调用 Native 的功能,但其核心是建立 Native 和 非 Native 间消息 双向通信 通道。


双向通信的通道

  • JavaScript 向 Native 发送消息:

    • 调用 Native 功能
    • 通知 Native 当前 JavaScript 的相关状态等
  • Native 向 JavaScript 发送消息:

    • 回溯调用结果
    • 消息推送
    • 通知 JavaScript 当前 Native 的状态等

JSBridge 是如何实现的?

JavaScript 的运行需要 JS 引擎的支持,包括 Chrome V8Firefox SpiderMonkeySafari JavaScriptCore 等,总之 JavaScript 运行环境 是和 原生运行环境 是天然隔离的,因此,在 JSBridge 的设计中我们可以把它 类比 成 JSONP 的流程:

  • 客户端通过 JavaScript 定义一个回调函数,如: function callback(res) {...},并把这个回调函数的名称以参数的形式发送给服务端
  • 服务端获取到 callback 并携带对应的返回数据,以 JS 脚本形式返回给客户端
  • 客户端接收并执行对应的 JS 脚本即可

JSBridge 实现 JavaScript 调用的方式有两种,如下:

  • JavaScript 调用 Native
  • Native 调用 JavaScript

在开始分析具体内容之前,还是有必要了解一下前置知识 WebView

WebView 是什么?

WebView 是 原生系统 用于 移动端 APP 嵌入 Web 的技术,方式是内置了一款高性能 webkit 内核浏览器,一般会在 SDK 中封装为一个 WebView 组件。

WebView 具有一般 View 的属性和设置外,还对 url 进行请求、页面加载、渲染、页面交互进行增强处理,提供更强大的功能。

WebView 的优势 在于当需要 更新页面布局 或 业务逻辑发生变更 时,能够更便捷的提供 APP 更新:

  • 对于 WebView 而言只需要修改前端部分的 Html、Css、JavaScript 等,通知用户端进行刷新即可
  • 对于 Native 而言需要修改前端内容后,再进行打包升级,重新发布,通知用户下载更新,安装后才可以使用最新的内容

微信小程序中的 WebView

小程序的主要开发语言是 JavaScript ,其中 逻辑层 和 渲染层 是分开的,分别运行在不同的线程中,而其中的渲染层就是运行在 WebView 上:

运行环境逻辑层渲染层
iOSJavaScriptCoreWKWebView
安卓V8chromium 定制内核
小程序开发者工具NWJSChrome WebView


在开发过程中遇到的一个 坑点 就是

  • 在真机中,需要实现同一域名下不同子路径的应用实现数据交互(纯前端操作,不涉及接口),由于同域名且是基于同一个页面进行跳转的(当然只是看起来是),而且这个数据是 临时数据,因此觉得使用 sessionStorage 实现数据交互是很合适的
  • 实际上从 A 应用 跳转到 B 应用 中却无法获取对应的数据,而这是因为 sessionStorage 是基于当前窗口的会话级的数据存储,移动端浏览器 或 微信内置浏览器 中在跳转新页面时,可能打开的是一个新的 WebView,这就相当于我们在浏览器中的一个新窗口中进行存储,因此是没办法读取在之前的窗口中存储的数据

JavaScript 调用 Native — 实现方案一

通过 JavaScript 调用 Native 的方式,又会分为:

  • 注入 API
  • 劫持 URL Scheme
  • 弹窗拦截

【 注入 API 】

核心原理

  • 通过 WebView 提供的接口,向 JavaScript 的上下文(window)中注入 对象 或者 方法
  • 允许 JavaScript 进行调用时,直接执行相应的 Native 代码逻辑,实现 JavaScript 调用 Native

这里不通过 iOS 的 UIWebView 和 WKWebView 注入方式来介绍了,感兴趣可以自行查找资料,咱们这里直接通过 微信 JS-SDK 来看看。


当通过  的方式引入 JS-SDK 之后,就可以在页面中使用和 微信相关的 API,例如:

// 微信授权
window.wx.config(wechatConfig)

// 授权回调
window.wx.ready(function () {...})

// 异常处理
window.wx.error(function (err) {...})

// 拉起微信卡包
window.wx.invoke('chooseInvoice', invokeConf, function (res) {...})

如果通过其内部编译打包后的代码(简化版)来看的话,其实不难发现:

  • 其中的 this(即参数 e)此时就是指向全局的 window 对象
  • 在代码中使用的 window.wx 实际上是 e.jWeixin 也是其中定义的 N 对象
  • 而在 N 对象中定义的各种方法实际上又是通过 e.WeixinJSBridge 上的方法来实际执行的
  • e.WeixinJSBridge 就是由 微信内置浏览器 向 window 对象中注入 WeiXinJsBridge 接口实现的

    !(function (e, n) {
    'function' == typeof define && (define.amd || define.cmd)
    ? define(function () {
    return n(e)
    })
    : n(e, !0)
    })(this, function (e, n) {
    ...
    function i(n, i, t) {
    e.WeixinJSBridge
    ? WeixinJSBridge.invoke(n, o(i), function (e) {
    c(n, e, t)
    })
    : u(n, t)
    }

    if (!e.jWeixin) {

    var N = {
    config(){
    i(...)
    },
    ready(){},
    error(){},
    ...
    }

    return (
    S.addEventListener(
    'error',callback1,
    !0
    ),
    S.addEventListener(
    'load',callback2,
    !0
    ),
    n && (e.wx = e.jWeixin = N),
    N
    )
    }
    })

【 劫持 URL Scheme 】

URL Scheme 是什么?

URL Scheme 是一种特殊的 URL,一般用于在 Web 端唤醒 App(或是跳转到 App 的某个页面),它能方便的实现 App 间互相调用(例如 QQ 和 微信 相互分享讯息)。

URL Scheme 的形式和 普通 URL(如:https://www.baidu.com)相似,主要区别是 protocol 和 host 一般是对应 APP 自定义的。

通常当 App 被安装后会在系统上注册一个 自定义的 URL Scheme,比如 weixin:// 这种,所以我们在手机浏览器里面访问这个 scheme 地址,系统就会唤起对应的 App

例如,当在浏览器中访问 weixin:// 时,浏览器就会询问你是否需要打开对应的 APP:


劫持原理

Web 端通过某种方式(如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL Scheme 和 携带的参数 进行对应操作。

例如,对于谷歌浏览器可以通过 chrome://version/、chrome://chrome-urls/、chrome://settings/ 定位到不同的页面内容,假设 跳转到谷歌的设置页并期望当前搜索引擎改为百度,可以这样设计 chrome://settings/engine?changeTo=baidu&callbak=callback_id:

  • 谷歌客户端可以拦截这个请求,去解析对应参数 changeTo 来修改默认引擎
  • 然后通过 WebView 上面的 callbacks 对象来根据 callback_id 进行回调

以上只是一个假设哈,并不是说真的可以这样去针对谷歌浏览器进行修改,当然它要是真的支持也不是不可以。

是不是感觉确实和 JSONP 的流程很相似呀 ~ ~

【 弹窗拦截 】

弹窗拦截核心:利用弹窗会触发 WebView 相应事件来实现的

一般是在通过拦截 Prompt、Confirm、Alert 等方法,然后解析它们传递过来的消息,但这种方法存在的缺陷就是 iOS 中的 UIWebView 不支持,而且 iOS 中的 WKWebView 又有更好的 scriptMessageHandler,因此很难统一。

Native 调用 JavaScript — 实现方案二

Native 调用 JavaScript 的方式本质就是 执行拼接 JavaScript 字符串,这就好比我们通过 eval() 函数来执行 JavaScript 字符串形式的代码一样,不同的系统也有相应的方法执行 JavaScript 脚本。

Android

在 Android 中需要根据版本来区分:

作者:熊的猫
来源:segmentfault.com/a/1190000043417038
收起阅读 »

我的2022,用爱发电

Hello,我是Xc,忙碌的一年结束了,先祝大家新年快乐,其实元旦之前就想写这篇文章,但是由于种种原因始终没有落实这个计划。 总结了2022年,这一年基本都是在吃饭睡觉写代码中度过的,对于我来说这一年有几个关键词:开源、成长、收获。 开源 为什么会选择开源这条...
继续阅读 »

Hello,我是Xc,忙碌的一年结束了,先祝大家新年快乐,其实元旦之前就想写这篇文章,但是由于种种原因始终没有落实这个计划。


总结了2022年,这一年基本都是在吃饭睡觉写代码中度过的,对于我来说这一年有几个关键词:开源成长收获


开源


为什么会选择开源这条路呢?


因为在21年的vueconf上被一个可以写代码的ppt吸引(slidev),当时觉得这个东西太酷了,这才是程序员PPT该有的样子,之后就一直关注antfu,在后续半年的关注中发现这个人怎么会有这么多的idea,能有这么高的产出,太惊人了。因为平时在三心老总的技术群里经常回答一些群友的问题,和看大家聊天会提到开源一词,心里逐渐有了一些想法,之后在21年年末的时候,大哥让大家想想自己的22年年度计划,毫不犹豫的想到了在掘金写点技术小作文和开源一些项目。


第一个开源项目


春节复工后就开始了自己的开源计划,正好当时有个业务需求需要对vite进行拓展支持,在考虑有没有敏感信息之后就打算将其作为开源项目。记得很清楚,2月10号发布了我的第一个npm包、第一个vite插件vite-plugin-dynamic-base(目前月下载量2-3K),并且完成了第一个pr,插件正式被收录到awesome-vite


image.png


成为Element Plus团队成员


在开源vite-plugin-dynamic-base之后也不知道还能在开源上还能做些什么了,在看了antfu的直播后豁然开朗,我不一定要自己产出项目来为开源生态做贡献,我可以通过给一些开源社区的项目维护来贡献。从vue2的组件库使用上来说,出于对element-ui的熟练和喜爱,所以就想说能否给vue3的Element Plus做些什么?


我还记得第一个pr是修复了select-v2的value-key的问题,还得级当时没有格式化,reviewer耐心的提示我如何操作提交。之后又接着修复input-number的issue,当时reviewer问我是否介意重构一下里面的几个方法,当时是又激动又紧张,激动的是收到到了reviewer的一个邀约(感觉是被信任),紧张是因为都是在github上面交流,我初来乍到不知道代码规范,当时就硬着头皮进行了代码的review,很高兴在reviewer的细心review下pr成功merge了。感觉动力十足,每天下班回家就看下issue反馈的问题,如果是使用上的问题会进行解答,如果是bug就分析存在bug的原因,然后尝试着修复。


还记得当时3-4月份的时候,由于21年圣诞节的一次意外导致右手关节错位,因为治疗的比较晚,只能打绷带来了,还好手指头还能敲代码哈,那时候也是一换完药就赶着回去看issue和修pr。之后陆续到了四月中旬,有幸收到Element Plus的合作者邀请,当时激动的不行,每天和打了鸡血一样,天天下班回去就是泡在Element Plus的仓库里面。


在之后到了5月中旬,有幸成为了团队成员,当时也是十分开心被维护者们认可,但是身份不一样感觉在回复issue的时候总是特别小心,生怕做错事。真的就是怕啥来啥,当时正赶上Element Plus的组件语法重构计划,当时有个变量没考虑到从原来的options API切换到setup语法后这个变量每次初始化的值都是一样的问题,导致用户系统出现bug,当时收到用户的pr,被骂是其次,主要是觉得自己的不严谨砸了Element Plus的口碑,那时候挺郁闷的,感觉自己是不是不太适合,多亏了团队其他小伙伴的鼓励,过了这个关卡。


image.png


羊了个羊


9月份的微信可以说是被羊了个羊霸占了,一开始觉得在朋友圈开到觉得挺无聊的,后面自己玩了一下一发不可收拾,每天下班回去路上都在玩这个。出于程序员的思维就在思考这个游戏是怎么实现的,但是和前端群的小伙伴讨论了下感觉也不是很难,就想着自己也做一个。通过数据结构讨论后,这个项目很快就产出了,当时还在说游戏以什么作为主题,碰巧公司需求提议说做成兔了个兔,就这样代码和默认主题的兔了个兔


10月25日的下午,突然收到群友的at,才知道自己的兔了个兔开源项目xlegex被阮一峰老师写到他的网络日志羊了个羊,如何自己实现里面了,又是开心的一天。


image.png


也通过阮一峰老师的日志和云游君的fork,我也收获了第一个百星项目 🙏


其他开源项目



成长


工作这些年,做过java+jsp/angular/vue再到后面的python人工智能算法,到最后决定专注在前端方向,就想好既然选择了前端就要保持不停的学习准备,在之前的工作中我也意识到,如果只靠工作上面的东西,是会成长,但是远远不够。我记得崔宝秋老师的一句话:只有你读了大量的代码,读了不同风格的代码,读了不同领域的代码,才能够真正提升自己的功底。当然这种读代码,还只是纸上谈兵,真正要成为一个编程高手必须写,读了很多高手的优质代码以后才能够快速提升自己写代码的能力。第二个我觉的要有对技术的爱... 开源正好很符合这个事,开源的一年,对我自己的技术水平有了明显的成长,在工作上的帮助也很大,能够有更多的解决方案去应对工作上的需求和问题,所以我也一直在鼓励群里的小伙伴人多参与开源,对自己的提升是很有帮助的。


收获



  • 收获了工作团队以外另一个很棒的团队,有幸认识了团队里的好多大佬,从他们身上学到了好多东西。

  • 收获一群前端水友,还记的那个review的夜晚,氛围真的太好的!!

  • 收获了百星项目。

  • 收获了工作上的认可和成就。


收获的太多太多了。。。


展望2023


2023再接再厉,保持开源的热情,继续成长,希望能产出一些更好的开源项目,收获更多的star~~


respect to 每一位用爱发电的开源作者~~


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

去年裸辞全职做开源后,趁快过年说说这一年的经历

刚裸辞全职搞开源时分享过一次,当时收到了很多网络友人们的鼓励和建议,当然也有不看好,很难挣到钱,经过一年多的实践,证明了他们说的是对的哈哈。 说说这一年的经历,大家就当听个故事,当然如果能给相似想法的朋友一个案例参考,那更好。 简述下故事的背景,2021 年 ...
继续阅读 »

刚裸辞全职搞开源时分享过一次,当时收到了很多网络友人们的鼓励和建议,当然也有不看好,很难挣到钱,经过一年多的实践,证明了他们说的是对的哈哈。


说说这一年的经历,大家就当听个故事,当然如果能给相似想法的朋友一个案例参考,那更好。


简述下故事的背景,2021 年 10 月底裸辞后,开始做一款 IT 监控告警产品,想法是基于 开源+SAAS 的模式去做,一款从零开始就开源的一款产品。github.com/dromara/her…


2021.11 到 2022.1 过年,基本就是在初版开发,这期间注册了公司,监控告警产品做到简单可用了,云服务 SAAS 的集群基本成型可用。开源方面,个人账户的开源项目说实话很难吸引到开发者,运营了一段时间我就把开源项目捐赠到我们 Dromara 社区,基于社区来传播和协作,事实证明效果确实比单打独斗强。


2022.2 到 2022.3 这个月就开始精彩了,网上遇到个客户说要买产品定制化部署到他们那,我当然很激动呀,之前一直是研发没有一线的经验,这哥们是运维,刚开始会问些他遇到的技术难点,后面是他自己想基于其它开源项目包装成两个产品卖给他自己公司,让我帮忙分析,还得把他发的开源项目的部署跑起来,还让我联系开发,报价,反正中间折腾了我非常多时间最后他嫌贵不做了,到后面有一天,他说我的产品正在公司部署,让我帮忙看看为啥 mysql 起不来,我远程进去 fix 了后顺便看了下数据库,这 tm 不是我产品的数据库名啊,问了才说这是其它厂商的数据库,他部署不起来。。。。。这 tm ,兄弟们,两个月啊,整整前后两个月时间,我至少花了整整半个月时间在他身上,这中间他会给我说在推进项目。。。。。就这样我被白嫖了断断续续两个月的技术支持加免费劳动力。


这件事情之后,后面有几家集成商找我开发帮忙把这监控项目整合进他们系统,卖了政府医院一起分钱,我的回复都是,请先预打款,当然对于习惯白嫖的他们来说,是没有下文了。


对了这期间还做了甲状腺手术,人生第一次进手术室,那环境真的跟电视里差不多。朋友们不要熬夜保护好你的甲状腺啊。


2022.4 到 2022.5 这段时间就是继续开发维护开源项目和云服务 SAAS ,开源项目还获得🉐️了 Gitee 最具价值开源项目 GVP ,这也是项目的一个肯定吧。开源项目还成为中科院的开源之夏活动的活动项目,学生们在暑期参与开发会有 1 万左右的奖金,我作为导师会有 3 千奖金哈哈。半年没收入的我看到这个还是很激动的。


2022.5 月,这个月。我去上班了,一家北京的公司联系到我,让我跟他们开发一款开源社区的产品,说的是看中我这方面的经验,薪资 23 ,我远程在家办公。考虑到这半年的被白嫖,无收入,还是就是马上娃娃要出生了,于是就暂缓了自己项目进度(下班后有空做做),去打工了。


2022.5 月 到 2022.12 这期间在打工,有空的时候也在维护我那个开源产品。虽说在打工,有趣的是,因为远程从入职到离开我都没有见过同事哈哈。


2022.12 月,这个月,我又辞职了。原因比较简单,老板学其它公司降薪,别人降 20%,他降一半😂。想着自己还略有发展的开源监控项目,虽然还有一个多月就过年,虽然之前说的 14 薪(我估计都悬毕竟年前降一半),也就不耗着了提了离职,好聚好散吧。


2022.12 到现在,感觉进入了一个循环,我又开始裸辞了哈哈。这一个多月我把云服务开启了高级版付费,然后开源项目的企业版本也搞出来了,希望在接下来的日子了,每个月能靠他们挣点奶粉钱。


总结,开源项目收入 0 ,企业版(刚弄出来)收入 0 , 云服务付费高级版(刚弄出来)收入 0 ,开源项目接收捐赠 300 左右,服务器域名CDN宣传发红包发开源周边礼物等支出 5K 左右。总 -4700 元😂。


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

中年Andorid的2022

又是一年过去了,又到了一年一度的年终总结。回看今年在掘金的输出,嗨,上一篇还是去年的年终总结,回看一下去年立下的flag,完成量不足一半啊。 工作和考研 那今年是不是蹉跎的一年呢,当然也不是,还记得去年的总结中有一条,考研哈哈,对于一个毕业有十年的老家伙来说做...
继续阅读 »

又是一年过去了,又到了一年一度的年终总结。回看今年在掘金的输出,嗨,上一篇还是去年的年终总结,回看一下去年立下的flag,完成量不足一半啊。


工作和考研


那今年是不是蹉跎的一年呢,当然也不是,还记得去年的总结中有一条,考研哈哈,对于一个毕业有十年的老家伙来说做这个决定都是被现实逼的,还不是为了以后万一哪天失业了,给自己增加一点资本哎。于是从2月份开始就开始准备了,调研学什么、怎么学。


当我们去调研的时候,肯定会看到很多培训班的广告,当然对于一个工作近10年,有自主学习能力的老程序员来说我不认为非得报班才可以,网上的资料完全够用,学什么呢,对于一个程序员来说必须计算机408走起,最终书买好,视频买好3月份就开始每天下班投入学习了,包括午休时间背背英语单词。要学的东西可真多5月份差不多学完了计算机组成原理、计算机网络、操作系统。数据结构没怎么学,因为工作这么多年找工作啥的都会用到,平时也经常会刷刷,然后就开始学数学了。


数学的难度明显上了一个档次,上次学高数还是大学的时候,平时工作中用到的数学的难度也就初中高中的难度🤦‍♂️,这导数微积分一上来就给整懵逼了,感觉数学需要练习的时间要顶408的4本书啊,路是自己选的,硬着头也得往下走啊。


时间来到了6月底,高数学了四分之三,线性代数和概率论还没开始,工作开始忙了起来,领导要对项目进行大重构,项目彻底组件化,并且性能、包体积、崩溃率等方面也都要达标,有幸参与了整个过程,对于这种大的重构,在之前的公司是没有做过的,这也是一种很好的实践和成长。


但是随之而来的就是工作量大增加,由于很多改动影响范围很大,包括很多年前的祖传代码,所以做起来也相当谨慎,测试文档也需要写的非常详细,争取不漏掉任何改动。有一段时间项目编译非常慢,有时候多个分支一起开发的时候,切一个分支那是相当痛苦,二三十分钟就出去了,因此有一段时间都是用两个电脑开发不同的分支。下班时间也开始越来越晚...。由于工作强度大,回家很晚,再加上数学很枯燥,于是从7月份开始考研的学习就中断了...哎


高强度的工作一直持续到11月份,虽然已经过去了一个多月,现在想想仿佛还在昨天,收获也不小,很幸运能经历这么一次重构,实践出真知,很多时候我们在网上学习的新技术或者解决某些问题的方法,真正的用一遍才能有更深的认识。


这时候离这考试就一个月多一点了,虽然没怎么学习,十月份报名的时候还是报了,就是想去体验一下,如果来年再战心里也有点底,如果以后不再战,也权当一次经历,此时还有一点时间,学肯定是学不完,看个政治、背个作文啥的抱抱佛脚还是可以的哈哈哈。


12月25骑着我心爱的小摩托一早就来到了学校,怀着平静的心情的走进了考场,经过两天的考试,我的首次考研经历结束,最惨就是数学了,大题一个不会直接交个白卷,从小到大第一次交白卷,真是丢人丢到家了,而且考试的时候,那种啥也不会还得在那里等着考试完,真的是非常煎熬,最后提前半小时交卷狼狈而逃~~。


疫情和生活


考试完了,无论好坏都算是了了一件心事。再说到疫情,今年北京的疫情一直都是比较严重的,进出京政策也一直非常严格,以至于今年前半年的假期都是在家窝着过的,还好孩子在身边。


后半年稍微好了点,7月份孩子跟着姥姥回老家了,那个周末我跟媳妇骑着小摩托到周边的凤凰岭爬了个山。


8月份弟弟放暑假带着他在北京玩了几天,去秦皇岛看了下海。十月份孩子终于又回到了北京。


十一月份疫情开始严重,居家办公三周,第四周去公司两天后阳了~~。嗓子疼一天,发烧两天,咳嗽4天经历7天时间转阴,感觉重活一世。


疫情放开后,经历了各种抢药、抢抗原,如今北京应该超过80%的阳过了,大家的生活似乎恢复到了正常,但是去一趟医院就知道还差很多,今年过年回家终于不用害怕因为疫情回不来了。不过最近大家又在抢调理肠胃的药这种生活目前不知道尽头在哪里,只能顺其自然,希望疫情早点结束。


去年攒了点钱还了部分房贷,房贷压力少了一些,今年继续努力,早日摆脱房贷。


明年



  • 恢复写博客,写东西这个还是不能落下不一定是博客,也可以是自己的笔记文档,写的时候能梳理逻辑,让思维更清晰,特别是学东西的时候,将新学的东西写出来,不仅能加深印象,写的时候还能将学的时候似懂非懂的地方想清楚。

  • 工作继续卷一卷,不倦不行啊,你不走我不走,看谁卷过谁。今年公司来了新的CTO,所以今年的项目优化也比较多,好好工作好好实践。

  • 疫情放开了,今年周末多跟家人去北京周边的景区玩玩。

  • 考研成绩出来后,根据情况看看今年是否继续~~~ , 这是个大活啊,人到中年,有时候时间真的不够用。年轻时候留下的债早晚都得还。

  • 读书 手机读书没感觉,租房又不愿买一堆纸质书 准备买个电子书用来读,做好笔记

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

因为使用 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

收起阅读 »