-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
486 lines (234 loc) · 311 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>米家拓展程序开发试用</title>
<link href="/archives/miot-extension-development-trial.html"/>
<url>/archives/miot-extension-development-trial.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><blockquote><p>米家扩展程序是米家 APP 中用于查看硬件产品实时信息和控制硬件产品的子程序。</p><p>小米 IoT 平台基于 <a href="https://facebook.github.io/react-native/" target="_blank" rel="noopener">React Native</a> 框架 (简称 RN 框架)。</p><p>平台提供了使用 JavaScript 语言开发成的扩展程序框架 SDK(地址: <a href="https://github.com/MiEcosystem/miot-plugin-sdk" target="_blank" rel="noopener">miot-plugin-sdk</a> ,下文简称 SDK)。<br>这个 SDK 是一个本地 npm 包。<br>开发者可以根据产品实际的功能,通过调用 SDK 中的模块(JavaScript 接口集合),开发维护硬件产品的扩展程序。</p></blockquote><a id="more"></a><p><img src="https://cdn.cnbj0.fds.api.mi-img.com/miio.files/commonfile_png_c4916ad56b69260d07c4e50afecda46e.png" alt=""></p><p>以上内容来自 <a href="https://iot.mi.com/new/doc/accesses/direct-access/extension-development/overview" target="_blank" rel="noopener">小米 IoT 开发者社区</a> ,有删改。</p><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>开发者可以使用 JavaScript 编写米家拓展程序,项目基于 React Native。</p><p>米家 APP 在其基础上提供了包括但不限于<br>基础 UI 组件、设备管理(Device)、调用终端资源(System)、使用米家 APP(Host)、与小米IoT 平台交互(Service)<br>等能力。</p><p>明白了这些就可以开始第一次米家拓展程序的开发。</p><h3 id="开发环境"><a href="#开发环境" class="headerlink" title="开发环境"></a>开发环境</h3><ol><li><p>安装 <a href="https://git-scm.com/downloads" target="_blank" rel="noopener">git</a></p></li><li><p>安装 <a href="https://nodejs.org/en/" target="_blank" rel="noopener">NodeJS</a></p><p>可以手动安装,也可以根据下文小米提供的一键安装脚本安装。</p></li></ol><h3 id="miot-plugin-sdk"><a href="#miot-plugin-sdk" class="headerlink" title="miot-plugin-sdk"></a>miot-plugin-sdk</h3><h4 id="SDK-下载"><a href="#SDK-下载" class="headerlink" title="SDK 下载"></a>SDK 下载</h4><p>访问 <a href="https://github.com/MiEcosystem/miot-plugin-sdk" target="_blank" rel="noopener">miot-plugin-sdk</a> 项目地址,<br>克隆项目以安装扩展程序框架 SDK。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git <span class="built_in">clone</span> [email protected]:MiEcosystem/miot-plugin-sdk.git</span><br></pre></td></tr></table></figure><blockquote><p>注:如果没有配置 github SSH 秘钥,需要先配置 SSH 秘钥,或者使用 https 的方式进行克隆。</p><p>git clone <a href="https://github.com/MiEcosystem/miot-plugin-sdk.git" target="_blank" rel="noopener">https://github.com/MiEcosystem/miot-plugin-sdk.git</a></p></blockquote><h4 id="环境安装"><a href="#环境安装" class="headerlink" title="环境安装"></a>环境安装</h4><p>如果没有安装相关的环境(见上文 <a href="#开发环境">开发环境</a> ),可以使用小米提供的一键安装脚本安装。</p><p>命令行进入开发环境根目录:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> miot-plugin-sdk</span><br></pre></td></tr></table></figure><p>Windows 电脑执行:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">bin/install_mihome_dev.bat</span><br></pre></td></tr></table></figure><p>Mac/Linux 电脑执行:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">chmod +x bin/install_mihome_dev.sh</span><br><span class="line">bin/install_mihome_dev.sh</span><br></pre></td></tr></table></figure><h4 id="安装-SDK-依赖基础库"><a href="#安装-SDK-依赖基础库" class="headerlink" title="安装 SDK 依赖基础库"></a>安装 SDK 依赖基础库</h4><p>项目基本上就是一个 NodeJS 项目,进入项目根目录执行 npm install:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> miot-plugin-sdk</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><ol><li>网络问题</li></ol><p>注意:在安装依赖过程中可能会报以下错误:</p><p><code>connect ECONNREFUSED 0.0.0.0:443</code></p><p>基本上就是因为中国大陆网络问题导致依赖下载失败,请自行解决网络问题并重试。</p><ol start="2"><li>fsevents 报错</li></ol><p>如果 Windows 下 fsevents 报错,可忽略。</p><p>Mac/Linux 下报错,可执行以下命令解决:</p><p><code>npm install fsevents@latest</code></p><h4 id="手机安装米家-APP-调试包"><a href="#手机安装米家-APP-调试包" class="headerlink" title="手机安装米家 APP 调试包"></a>手机安装米家 APP 调试包</h4><p>参考 <a href="https://github.com/MiEcosystem/miot-plugin-sdk" target="_blank" rel="noopener">miot-plugin-sdk</a> 中小米提供的下载链接进行下载安装:</p><p><a href="https://github.com/MiEcosystem/miot-plugin-sdk#%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83" target="_blank" rel="noopener">https://github.com/MiEcosystem/miot-plugin-sdk#%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83</a></p><p>这是米家 APP 的测试安装包,该安装包作为拓展程序的原生载体,<br>用以承载运行开发者编写的 RN 版本的米家拓展程序。</p><h4 id="运行小米提供的-Demo-插件"><a href="#运行小米提供的-Demo-插件" class="headerlink" title="运行小米提供的 Demo 插件"></a>运行小米提供的 Demo 插件</h4><p>由于小米提供的是本地 npm 包,故插件项目需要和 <a href="https://github.com/MiEcosystem/miot-plugin-sdk" target="_blank" rel="noopener">miot-plugin-sdk</a><br>放在一起。</p><p>插件项目位于 <a href="https://github.com/MiEcosystem/miot-plugin-sdk/tree/master/projects" target="_blank" rel="noopener">miot-plugin-sdk/projects</a> 。</p><p>这个目录下的所有插件都以包名命名,作为 <a href="https://github.com/MiEcosystem/miot-plugin-sdk" target="_blank" rel="noopener">miot-plugin-sdk</a><br>的子 NodeJS 项目存在。</p><p>由于每一个插件都是一个 NodeJS 项目,故插件项目如有依赖额外的 npm 包的话也需要 install。</p><ol><li>运行 demo 项目</li></ol><p>进入 demo 项目安装依赖:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> projects</span><br><span class="line"><span class="built_in">cd</span> com.xiaomi.demo</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p>返回根目录执行运行插件命令:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ../..</span><br><span class="line">npm run com.xiaomi.demo</span><br></pre></td></tr></table></figure><p>如无意外的话,插件项目编译成功后会在终端上打印出一个二维码:</p><p><img src="https://img-cdn.pek3b.qingstor.com/blog/miot-build-success.png" alt=""></p><p>实际上这个二维码就是简单的 json 文本:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"ip"</span>: <span class="string">"192.168.2.104"</span>,</span><br><span class="line"> <span class="attr">"package"</span>: <span class="string">"com.xiaomi.demo"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>开发者也可以自己根据电脑本地 ip 和包名生成二维码。</p><ol start="2"><li>手机扫码体验</li></ol><p>现在可以使用手机打开上文中安装的米家 APP。</p><p>确保手机和电脑处于一个 wifi 环境,且无线路由器没有开启 AP 隔离。</p><p>然后扫描上面的二维码。</p><p>此时手机会提示出于 debug 模式:</p><p><img src="https://img-cdn.pek3b.qingstor.com/blog/IMG_6D41E978D3A8-1.jpeg" alt=""></p><p>此时打开任何已经安装的米家插件都将打开开发者运行的项目。</p><p>如果你和我一样暂时并没有关联任何米家产品的话,可以手动添加:</p><p>点击右上角加号 -> 添加设备 -> 滑动到最下方 “其他” 分组 -> 选择 “红外遥控” -> 选择 “自定义遥控”</p><p>此时 APP 会提示你出于开发者模式,点击确定即可打开运行的插件:</p><p><img src="https://img-cdn.pek3b.qingstor.com/blog/IMG_99E27C8E8E33-1.jpeg" alt=""></p><p>此时开发者电脑的终端会实时编译 JS 文件到 RN bundle 文件:</p><p><img src="https://img-cdn.pek3b.qingstor.com/blog/miot-plugin-load.png" alt=""></p><p>编译完成后就可以在手机上看到 demo 插件的预览界面:</p><p><img src="https://img-cdn.pek3b.qingstor.com/blog/IMG_F8D3BC5FC898-1.jpeg" alt=""></p><p>现阶段请暂时忽略 APP 提示的插件代码里面的黄色警告信息。</p><h4 id="自行编写插件"><a href="#自行编写插件" class="headerlink" title="自行编写插件"></a>自行编写插件</h4><p>TBD</p><!-- rebuild by neat -->]]></content>
<categories>
<category> React Native </category>
</categories>
<tags>
<tag> React Native </tag>
<tag> miot </tag>
<tag> mi </tag>
</tags>
</entry>
<entry>
<title>Kotlin 中的 let, with, run, apply, also 等函数的使用</title>
<link href="/archives/fun-let-with-run-apply-also-in-kotlin.html"/>
<url>/archives/fun-let-with-run-apply-also-in-kotlin.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>和严格古老的 Java 相比,Kotlin 中额外提供了不少高级语法特性。<br>这些高级特性中,定义于 Kotlin 的 <a href="https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Standard.kt" target="_blank" rel="noopener">Standard.kt</a><br>为我们提供了一些内置拓展函数以方便我们写出更优雅的代码。</p><p>相比大多数人都用过 let 函数来做过 Null Check,和 let 函数一样,with, run, apply, also 都可以提供非常强大的功能用以优化代码。</p><a id="more"></a><h3 id="let"><a href="#let" class="headerlink" title="let"></a>let</h3><p>当需要定义一个变量在一个特定的作用域时,可以考虑使用 let 函数。当然,更多的是用于避免 Null 判断。</p><p>在 let 函数内部,用 it 指代调用 let 函数的对象,并且最后返回最后的<strong>计算值</strong></p><h4 id="一般结构"><a href="#一般结构" class="headerlink" title="一般结构"></a>一般结构</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">any.let {</span><br><span class="line"> <span class="comment">// 用 it 指代 any 对象</span></span><br><span class="line"> <span class="comment">// todo() 是 any 对象的共有属性或方法</span></span><br><span class="line"> <span class="comment">// it.todo() 的返回值作为 let 函数的返回值返回</span></span><br><span class="line"> it.todo() </span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 另一种用法</span></span><br><span class="line">any?.let {</span><br><span class="line"> it.todo() <span class="comment">// any 不为 null 时才会调用 let 函数</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="具体使用"><a href="#具体使用" class="headerlink" title="具体使用"></a>具体使用</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> result = <span class="string">"Test"</span>.let {</span><br><span class="line"> println(it) <span class="comment">// Test</span></span><br><span class="line"> <span class="number">3</span> * <span class="number">4</span> <span class="comment">// result = 12</span></span><br><span class="line"> }</span><br><span class="line"> println(result) <span class="comment">// 12</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对应到实际使用场景一般是 需要对一个可能为 null 的对象多次做空判断:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">textView?.text = <span class="string">"TextSetInTextView"</span></span><br><span class="line">textView?.setTextColor(ContextCompat.getColor(<span class="keyword">this</span>, R.color.colorAccent))</span><br><span class="line">textView?.textSize = <span class="number">18</span>f</span><br></pre></td></tr></table></figure><p>使用 let 函数优化后:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">textView?.let { </span><br><span class="line">it.text = <span class="string">"TextSetInTextView"</span></span><br><span class="line">it.setTextColor(ContextCompat.getColor(<span class="keyword">this</span>, R.color.colorAccent))</span><br><span class="line">it.textSize = <span class="number">18</span>f</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="with"><a href="#with" class="headerlink" title="with"></a>with</h3><p>和 let 类似,又和 let 不同,with 最后也包含一段函数块,也是将最后的计算的结果返回。</p><p>但是 with 不是以拓展的形式存在的。其将某个对象作为函数的参数,并且以 this 指代。</p><p>首先来看 with 的一般结构:</p><h4 id="一般结构-1"><a href="#一般结构-1" class="headerlink" title="一般结构"></a>一般结构</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">whith(any) {</span><br><span class="line"> <span class="comment">// todo() 是 any 对象的共有属性或方法</span></span><br><span class="line"> <span class="comment">// todo() 的返回值作为 with 函数的返回值返回</span></span><br><span class="line"> todo() </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其实 with 函数的原始写法应该是:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">with(any, {</span><br><span class="line"> todo()</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>有用过 Groove DSL 的同学一定都知道在 Groovy 中,函数调用的最后一个参数是函数的话,函数的大括号可以提到圆括号() 的外面。</p><p>巧了,Kotlin DSL 也支持,所以最终就变成了<strong>一般结构</strong>中的那种写法了。</p><p>没错,Kotlin 也是支持 DSL 的,Android 使用 Gradle 进行编译,<code>build.gradle</code> 使用 Groovy 进行编写。</p><p>如果你对 Groovy 不太熟悉的话,也可以使用 Kotlin DSL 来写 <code>build.gradle.kts</code>。</p><h4 id="具体使用-1"><a href="#具体使用-1" class="headerlink" title="具体使用"></a>具体使用</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Person</span></span>(<span class="keyword">val</span> name: String, <span class="keyword">val</span> age: <span class="built_in">Int</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> chengww = Person(<span class="string">"chengww"</span>, <span class="number">18</span>)</span><br><span class="line"> <span class="keyword">val</span> result = with(chengww) {</span><br><span class="line"> println(<span class="string">"Greetings. My name is <span class="variable">$name</span>, I am <span class="variable">$age</span> years old."</span>)</span><br><span class="line"> <span class="number">3</span> * <span class="number">4</span> <span class="comment">// result = 12</span></span><br><span class="line"> }</span><br><span class="line"> println(result)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 let 函数的实际使用中,我们对 textView 进行空判断,但是每次函数调用的时候还是要使用 it 对象去调用。</p><p>如果我们使用 with 函数的话,由于代码块中传入的是 this,而不是 it,那么我们就可以直接写出函数名(属性)来进行相应的设置:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (textView == <span class="literal">null</span>) <span class="keyword">return</span></span><br><span class="line">with(textView) {</span><br><span class="line">text = <span class="string">"TextSetInTextView"</span></span><br><span class="line">setTextColor(ContextCompat.getColor(<span class="keyword">this</span><span class="symbol">@TestActivity</span>, R.color.colorAccent))</span><br><span class="line">textSize = <span class="number">18</span>f</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段代码唯一的缺点就是要事先判空了,有没有既能像 let 那样能优雅的判空,又能写出这样的便利的代码呢?</p><p>别着急,咱们接着往下看。</p><h3 id="run"><a href="#run" class="headerlink" title="run"></a>run</h3><p>刚刚说到,我们想能有 let 函数那样又优雅的判空,又能有 with 函数省去同一个对象多次设置属性的便捷写法。</p><p>没错,就是这就非我们 run 函数莫属了。run 函数基本是 let 和 with 的结合体,对象调用 run 函数,接收一个 lambda 函数为参数,传入 this 并以闭包形式返回,返回值是最后的计算结果。</p><h4 id="一般结构-2"><a href="#一般结构-2" class="headerlink" title="一般结构"></a>一般结构</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">any.run {</span><br><span class="line"> <span class="comment">// todo() 是 any 对象的共有属性或方法</span></span><br><span class="line"> <span class="comment">// todo() 的返回值作为 run 函数的返回值返回</span></span><br><span class="line"> todo() </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么上面 TextView 设置各种属性的优化写法就是这样的:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">textView?.run {</span><br><span class="line">text = <span class="string">"TextSetInTextView"</span></span><br><span class="line">setTextColor(ContextCompat.getColor(<span class="keyword">this</span><span class="symbol">@TestActivity</span>, R.color.colorAccent))</span><br><span class="line">textSize = <span class="number">18</span>f</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>像上面这个例子,在需要多次设置属性,但设置属性后返回值不是改对象(或无返回值:Unit)不能链式调用的时候,就非常适合使用 run 函数。</p><h3 id="apply"><a href="#apply" class="headerlink" title="apply"></a>apply</h3><p>apply 函数和 run 函数很像,但是 apply 最后返回的是调用对象自身。</p><h4 id="一般结构-3"><a href="#一般结构-3" class="headerlink" title="一般结构"></a>一般结构</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> result = any.apply {</span><br><span class="line"> <span class="comment">// todo() 是 any 对象的共有属性或方法</span></span><br><span class="line"> todo() </span><br><span class="line"> <span class="number">3</span> * <span class="number">4</span> <span class="comment">// 最后返回的是 any 对象,而不是 12</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">println(result) <span class="comment">// 打印的是 any 对象</span></span><br></pre></td></tr></table></figure><p>由于 apply 函数返回的是调用对象自身,我们可以借助 apply 函数的特性进行多级判空。</p><h4 id="具体使用-2"><a href="#具体使用-2" class="headerlink" title="具体使用"></a>具体使用</h4><p>在 Java 中多级判空一直是老大难的问题:</p><p>下面是一个 School 类中包含内部类 Class,在 Class 又包含内部类 Student,我们想获取该 Student 的 name 属性的示例。</p><p>这其中包含对 className 的修改操作。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> School school = init();</span><br><span class="line"> <span class="comment">// To change the className of the a student and get his(her) name in this school what we should do in Java</span></span><br><span class="line"> <span class="keyword">if</span> (school != <span class="keyword">null</span> && school.mClass != <span class="keyword">null</span>) {</span><br><span class="line"> school.mClass.className = <span class="string">"Class 1"</span>;</span><br><span class="line"> System.out.println(<span class="string">"Class name has been changed as Class 1."</span>);</span><br><span class="line"> <span class="keyword">if</span> (school.mClass.student != <span class="keyword">null</span>) {</span><br><span class="line"> System.out.println(<span class="string">"The student's name is "</span> + school.mClass.student.name);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">static</span> School <span class="title">init</span><span class="params">()</span> </span>{</span><br><span class="line"> School school = <span class="keyword">new</span> School();</span><br><span class="line"> school.mClass = <span class="keyword">new</span> School.Class();</span><br><span class="line"> school.mClass.student = <span class="keyword">new</span> School.Class.Student();</span><br><span class="line"> school.mClass.student.name = <span class="string">"chengww"</span>;</span><br><span class="line"> <span class="keyword">return</span> school;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">School</span> </span>{</span><br><span class="line"> Class mClass;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Class</span> </span>{</span><br><span class="line"> String className;</span><br><span class="line"> Student student;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Student</span> </span>{</span><br><span class="line"> String name;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际情况中可能会有更多的判空层级,如果我们用 Kotlin 的 apply 函数来操作又会是怎么样呢?</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> school = init()</span><br><span class="line"> school?.mClass?.apply {</span><br><span class="line"> className = <span class="string">"Class 1"</span></span><br><span class="line"> println(<span class="string">"Class name has been changed as Class 1."</span>)</span><br><span class="line"> }?.student?.name?.also { println(<span class="string">"The student's name is <span class="variable">$it</span>."</span>) }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">init</span><span class="params">()</span></span>: School = School(School.Class(School.Class.Student(<span class="string">"chengww"</span>)))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">School</span></span>(<span class="keyword">var</span> mClass: Class? = <span class="literal">null</span>) {</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">Class</span></span>(<span class="keyword">var</span> student: Student? = <span class="literal">null</span>, <span class="keyword">var</span> className: String? = <span class="literal">null</span>) {</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">Student</span></span>(<span class="keyword">var</span> name: String? = <span class="literal">null</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="also"><a href="#also" class="headerlink" title="also"></a>also</h3><p>有没有注意到上面的示例中,我们最后打印该学生的名字的时候,调用了 also 函数。</p><p>没错,和 let 函数类似,唯一的区别就是 also 函数的返回值是调用对象本身,在上例中 also 函数将返回 <code>school.mClass.student.name</code>。</p><h4 id="一般结构-4"><a href="#一般结构-4" class="headerlink" title="一般结构"></a>一般结构</h4><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> result = any.also {</span><br><span class="line"> <span class="comment">// 用 it 指代 any 对象</span></span><br><span class="line"> <span class="comment">// todo() 是 any 对象的共有属性或方法</span></span><br><span class="line"> it.todo()</span><br><span class="line"> <span class="number">3</span> * <span class="number">4</span> <span class="comment">// 将返回 any 对象,而不是 12</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>函数定义见下表:</p><table><thead><tr><th>函数名</th><th>实现</th></tr></thead><tbody><tr><td>let</td><td>public inline fun <T, R> T.let(block: (T) -> R): R = block(this)</td></tr><tr><td>with</td><td>public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()</td></tr><tr><td>run</td><td>public inline fun <T, R> T.run(block: T.() -> R): R = block()</td></tr><tr><td>apply</td><td>public inline fun<t>T.apply(block: T.() -> Unit): T { block(); return this }</t></td></tr><tr><td>also</td><td>public inline fun T.also(block: (T) -> Unit): T { block(this); return this }</td></tr></tbody></table><p>具体的调用情况见下图:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/kotlin-fun-useage.png" alt="kotlin-fun-useage"></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Kotlin </category>
</categories>
<tags>
<tag> Kotlin </tag>
</tags>
</entry>
<entry>
<title>CSS 实现 3D 旋转立方体</title>
<link href="/archives/css-cube.html"/>
<url>/archives/css-cube.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>最近对 CSS3 的一些新特性比较感兴趣,经常可以碰到自己没见过的 CSS 特性。今天就利用 CSS 的 Z 轴旋转和位移来做一个 3D 可视化立方体。</p><p>预览页面:</p><p><a href="https://demo.chengww.com/css-cube/" target="_blank">https://demo.chengww.com/css-cube/</a><br>预览视频:</p><center><video webkit-playsinline="true" src="https://img-cdn.pek3b.qingstor.com/cube/css-cube.mp4" controls style="width:70%"></video></center><p>源代码:<br><a href="https://github.com/chengww5217/css-cube" target="_blank">https://github.com/chengww5217/css-cube</a></p><a id="more"></a><h3 id="放置立方体的-6-个面"><a href="#放置立方体的-6-个面" class="headerlink" title="放置立方体的 6 个面"></a>放置立方体的 6 个面</h3><p>定义 3D 元素的视距:</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">html</span> {</span><br><span class="line"> <span class="attribute">-webkit-perspective</span>: <span class="number">800px</span>;</span><br><span class="line"> <span class="attribute">perspective</span>: <span class="number">800px</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>perspective 属性允许改变 3D 元素查看 3D 元素的视图。当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。</p></blockquote><p>首先一个立方体是有 6 个面的,我们每个面放一张图片:</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"cube cube-wrapper"</span> <span class="attr">id</span>=<span class="string">"cube-rotate"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-left"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"1.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-right"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"2.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-top"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"3.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-bottom"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"4.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-end"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"6.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"box-front"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"5.png"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure><p>然后我们需要将这 6 个 div 叠在一起,不能让其直接上下排列。</p><p>设定 .cube-wrapper 内的子 div 绝对定位,让其脱离文档流:</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-class">.cube-wrapper</span> > <span class="selector-tag">div</span> {</span><br><span class="line"> <span class="attribute">position</span>: absolute;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在我们 6 个面就堆叠在一起了。</p><p>具体效果参考:<br><a href="https://demo.chengww.com/css-cube/#step1" target="_blank">https://demo.chengww.com/css-cube/#step1</a></p><p>现在我们就开始将每一个面按照设定的方向进行移动。</p><h3 id="移动"><a href="#移动" class="headerlink" title="移动"></a>移动</h3><p>将每一个面都移动到它对应的地方,比如左面:</p><p><a href="https://demo.chengww.com/css-cube/#step2" target="_blank">https://demo.chengww.com/css-cube/#step2</a></p><p>具体操作是:沿 Y 轴旋转 90°,然后沿着 Z 轴负方向移动立方体边长的一半</p><p>首先定义下参数:</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-pseudo">:root</span> {</span><br><span class="line"> <span class="attribute">--cube-width</span>: <span class="number">220px</span>; <span class="comment">/* 立方体边长 */</span></span><br><span class="line"> <span class="attribute">--transfrom-width</span>: <span class="built_in">calc</span>(var(--cube-width)/<span class="number">2</span>); <span class="comment">/* 沿 Z 轴正方向移动的位移 */</span></span><br><span class="line"> <span class="attribute">--transfrom-width-negative</span>: <span class="built_in">calc</span>(var(--cube-width)/-<span class="number">2</span>); <span class="comment">/* 沿 Z 轴负方向移动的位移 */</span></span><br><span class="line"> <span class="attribute">--cube-margin</span>: <span class="number">180px</span> <span class="comment">/* 设置立方体上下边距,避免立方体旋转而覆盖网页上下内容 */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其次定义左平移动画:</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* every side of the cube */</span></span><br><span class="line"><span class="comment">/* 为了兼容 webkit 内核,这里所有属性都加了 -webkit-xxx 如不需要兼容可以去掉*/</span></span><br><span class="line"><span class="selector-class">.box-left</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateY</span>(90deg) <span class="built_in">translateZ</span>(var(--transfrom-width-negative));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box-right</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateY</span>(90deg) <span class="built_in">translateZ</span>(var(--transfrom-width));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box-top</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(90deg) <span class="built_in">translateZ</span>(var(--transfrom-width));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box-bottom</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(90deg) <span class="built_in">translateZ</span>(var(--transfrom-width-negative));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box-front</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">translateZ</span>(var(--transfrom-width));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box-end</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">translateZ</span>(var(--transfrom-width-negative));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>说明下, 0 -25% 做了 90°旋转,从 25% - 50% 什么都没做,是为了模拟暂停效果。</p><p>因为总动画时长是 4s,每一个 25% 就是 1s,这里会暂停 1s。</p><p>50% - 75% 做了 Z 轴方向的位移,75% - 100% 同理,模拟暂停效果。</p><h3 id="其他几个面重复移动操作"><a href="#其他几个面重复移动操作" class="headerlink" title="其他几个面重复移动操作"></a>其他几个面重复移动操作</h3><p>同理将其他几个面也旋转到指定的位置,最后将整个立方体添加旋转动画即可</p><p><a href="https://demo.chengww.com/css-cube/#step3" target="_blank">https://demo.chengww.com/css-cube/#step3</a></p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* cube animation */</span></span><br><span class="line"><span class="selector-id">#cube-rotate</span> {</span><br><span class="line"> <span class="attribute">-webkit-animation</span>: rotate <span class="number">25s</span> linear infinite;</span><br><span class="line"> <span class="attribute">-webkit-transform-style</span>: preserve-<span class="number">3</span>d;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">@-<span class="keyword">webkit</span>-<span class="keyword">keyframes</span> rotate {</span><br><span class="line"> <span class="selector-tag">from</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(0) <span class="built_in">rotateZ</span>(0);</span><br><span class="line"> }</span><br><span class="line"> <span class="selector-tag">to</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(1turn) <span class="built_in">rotateZ</span>(1turn);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="Dom-元素处于屏幕可见区时才播放动画"><a href="#Dom-元素处于屏幕可见区时才播放动画" class="headerlink" title="Dom 元素处于屏幕可见区时才播放动画"></a>Dom 元素处于屏幕可见区时才播放动画</h3><p>该页面中包含多个动画(下面有我写的步骤示例动画),如果每一个动画都直接播放的话,在移动端上性能会很差。</p><p>现在我们优化下,仅在可见区时才播放动画。</p><h4 id="动画开关"><a href="#动画开关" class="headerlink" title="动画开关"></a>动画开关</h4><p>首先我们通过为元素设定/移除各种预先定义好的动画 class 来控制动画的加载。</p><p>比如我们定义立方体的最终旋转动画为:</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* cube animation */</span></span><br><span class="line"><span class="selector-class">.cube-rotate</span> {</span><br><span class="line"> <span class="attribute">-webkit-animation</span>: rotate <span class="number">25s</span> linear infinite;</span><br><span class="line"> <span class="attribute">-webkit-transform-style</span>: preserve-<span class="number">3</span>d;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">@-<span class="keyword">webkit</span>-<span class="keyword">keyframes</span> rotate {</span><br><span class="line"> <span class="selector-tag">from</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(0) <span class="built_in">rotateZ</span>(0);</span><br><span class="line"> }</span><br><span class="line"> <span class="selector-tag">to</span> {</span><br><span class="line"> <span class="attribute">-webkit-transform</span>: <span class="built_in">rotateX</span>(1turn) <span class="built_in">rotateZ</span>(1turn);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>没错,就是将上一段代码的 id 选择器换成类选择器。这样我们通过预先定义元素和类加载器同名 id,然后通过 id 获取元素,最后判断其是否可见来增删 class:</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (isElementInViewport(target)) {</span><br><span class="line">target.classList.add(target.id)</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line">target.classList.remove(target.id)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>判断元素是否可见也非常简单,<code>getBoundingClientRect</code> 函数可以获取元素的大小及其相对于视口的位置。</p><p>然后判断其底部坐标是否小于 <code>window.innerHeight || document.documentElement.clientHeight</code></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">isElementInViewport</span> (<span class="params">el</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> rect = el.getBoundingClientRect();</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> rect.top >= <span class="number">-100</span> &&</span><br><span class="line"> rect.left >= <span class="number">0</span> &&</span><br><span class="line"> rect.bottom <= (<span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight) && <span class="comment">/*or $(window).height() */</span></span><br><span class="line"> rect.right <= (<span class="built_in">window</span>.innerWidth || <span class="built_in">document</span>.documentElement.clientWidth) <span class="comment">/*or $(window).width() */</span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="事件监听"><a href="#事件监听" class="headerlink" title="事件监听"></a>事件监听</h3><p>动画开关的边界条件确定了之后,最后只需要添加滚动事件监听即可:</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">autoAnimate(<span class="string">'cube-rotate'</span>,</span><br><span class="line"> <span class="string">'transform-left-repeat'</span>, <span class="string">'transform-right-repeat'</span>,</span><br><span class="line"> <span class="string">'transform-top-repeat'</span>, <span class="string">'transform-bottom-repeat'</span>,</span><br><span class="line"> <span class="string">'transform-front-repeat'</span>, <span class="string">'transform-end-repeat'</span>)</span><br><span class="line"><span class="built_in">document</span>.getElementById(<span class="string">'cube-rotate'</span>).classList.add(<span class="string">'cube-rotate'</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">autoAnimate</span>(<span class="params">...elementIds</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> targets = []</span><br><span class="line"> elementIds.forEach(<span class="function"><span class="params">id</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> target = <span class="built_in">document</span>.getElementById(id)</span><br><span class="line"> <span class="keyword">if</span> (target) targets.push(target)</span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">let</span> windowHeight = <span class="built_in">document</span>.documentElement.clientHeight</span><br><span class="line"> <span class="built_in">window</span>.onresize = <span class="function"><span class="params">()</span> =></span> windowHeight = <span class="built_in">document</span>.documentElement.clientHeight</span><br><span class="line"> <span class="built_in">window</span>.scroll(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> targets.forEach(<span class="function"><span class="params">target</span> =></span> target.dTop = <span class="built_in">document</span>.scrollTop)</span><br><span class="line"> })</span><br><span class="line"> <span class="built_in">document</span>.addEventListener(<span class="string">'scroll'</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> scrollTop = <span class="built_in">document</span>.documentElement.scrollTop</span><br><span class="line"> targets.forEach(<span class="function"><span class="params">target</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (isElementInViewport(target)) {</span><br><span class="line"> target.classList.add(target.id)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> target.classList.remove(target.id)</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">isElementInViewport</span> (<span class="params">el</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> rect = el.getBoundingClientRect();</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> rect.top >= <span class="number">-100</span> &&</span><br><span class="line"> rect.left >= <span class="number">0</span> &&</span><br><span class="line"> rect.bottom <= (<span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight) &&</span><br><span class="line"> rect.right <= (<span class="built_in">window</span>.innerWidth || <span class="built_in">document</span>.documentElement.clientWidth)</span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><!-- rebuild by neat -->]]></content>
<categories>
<category> Web-Front-End </category>
</categories>
<tags>
<tag> HTML </tag>
<tag> CSS </tag>
<tag> 动画 </tag>
</tags>
</entry>
<entry>
<title>蜗牛星际 - 高性价比 nas,矿渣折腾之路</title>
<link href="/archives/woniuxingji.html"/>
<url>/archives/woniuxingji.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>之前回家的时候帮老娘下载韩剧的时候就听家里人抱怨不会下载电视剧。<br>虽说开个视频 APP 的 VIP 也凑合,但是我老娘就喜欢看韩剧,这玩意因为众所周知的原因都不引进了。<br>那么只有去论坛找字幕组下载了。教会我老娘这样的中年人去论坛用 bt 软件下载似乎非常有难度。<br>在那时我就萌生了一个自己搞一台 nas 放家里,然后远程帮忙下剧的心思。</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/synosogy-DS620slim.png" alt="synosogy-DS620slim"></p><a id="more"></a><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>Okay,先简单梳理下需求:</p><ol><li>大容量,可一次保存多部视频</li><li>方便远程控制</li><li>下载工具要齐全,bt,百度云等</li><li>7*24 小时待机,有比较好的视频 APP 支持</li><li>便宜,预算在 500 以内</li></ol><p>基于 <code>1-4</code>,除了 nas 外似乎没有其他更好的选择。</p><p>说起 nas,市场上已经有群晖、铁威马等厂商已经支持的比较好了。而且配套的附件 APP 也很完善。可惜只有一个缺点:贵,无法通过 <code>5</code>。</p><p>似乎这个需求已经无解了,就在这时我们的主角出现了 – 蜗牛星际。</p><h2 id="蜗牛星际"><a href="#蜗牛星际" class="headerlink" title="蜗牛星际"></a>蜗牛星际</h2><blockquote><p>“蜗牛星际”一机双挖服务器是由中原硅谷、IPFS 实验室联合开发的一款针对为 IPFS&CAI 城市服务节点提供存储服务的一款专业技术型计算机。它具有稳定、绿色节能、噪音低、操作简单、即插即用、数据处理速度快等特点。</p><p>CAI 是一个可扩展、分布式、隐私授权与自我审查的弹性区块链存储基础设施,是第一个支持流媒体大数据存储、直播、分发、增值等服务的公链项目。同时也是全球唯一存储式应用生态,它解决的是区块链 2.0 技术存在的瓶颈。</p><p>CAI 立志成为流媒体与大数据领域世界最大的有价数据区块链平台,通过 P2P 网络、超级节点、隐私授权、自我审查等核心技术为用户赋能数据价值;并通过实体通证经济模型实现客户需求与实体企业的数据流通,赋能实体经济。</p></blockquote><p><img src="https://pek3b.qingstor.com/img-cdn/blog/woniuxingji-front.jpg" alt="woniuxingji-front"></p><p>看看上面的官方描述,是不是很高大上?没错,这玩意就是矿机挖矿的骗局!蜗牛星际就是矿机的名字,现在已经不能挖矿,所以低价出售在市场上。</p><p>从最开始的 5000+ 一套,4 个月的时间,从峰顶跌落谷底。到最后清盘的时候,挖矿的成本远大于收入,矿机不开机也亏损。一开机,都不够电费。很多矿老板清盘,矿机贩子低价收矿机,然后低价倒卖。虽然这个机器仍然有很多不足,但是不到300元的售价,还是非常值得入手的。</p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><table><thead><tr><th>名称</th><th>型号</th><th>详细说明</th></tr></thead><tbody><tr><td>CPU</td><td>j1900</td><td></td></tr><tr><td>GPU</td><td>核显</td><td></td></tr><tr><td>RAM</td><td>4G</td><td>杂牌</td></tr><tr><td>硬盘</td><td>16G mSATA + 1 SATA</td><td>自带的 16G 固态实在是太渣</td></tr><tr><td>电源</td><td>150w</td><td>zunmax,普通杂牌,缩水严重</td></tr><tr><td>硬盘位</td><td>4</td><td>抽拉式硬盘架</td></tr></tbody></table><p>从上面这个表格可以看出,主机的硬件成本并不高。</p><ul><li><strong>CPU 与内存:</strong>CPU 就那样,核显也弱的一笔,内存就是普通的笔记本 d3 低压条,但是用也不算太大影响。</li><li><strong>硬盘:</strong>16GB 的 mSATA SSD,性能实在是太差,写入只有 3M,只能换了。</li><li><strong>硬盘架:</strong>主机最大的优点是有 4 个抽拉式硬盘架,可以做小型 nas 用。但是主机电源很渣,带 4 个 3.5 寸硬盘怕不是要炸,所以电源也换了。</li></ul><h3 id="主要接口"><a href="#主要接口" class="headerlink" title="主要接口"></a>主要接口</h3><table><thead><tr><th>名称</th><th>数量</th><th>详细说明</th></tr></thead><tbody><tr><td>显示接口</td><td>2</td><td>VGA<em>1 HDMI</em>1</td></tr><tr><td>USB 2.0</td><td>6</td><td>前 2 后 2 内 2</td></tr><tr><td>USB 3.0</td><td>2</td><td>后 2</td></tr><tr><td>网线 G 口</td><td>1</td><td>Intel i211 原生千兆</td></tr><tr><td>风扇三针</td><td>2</td><td></td></tr><tr><td>mSATA</td><td>1</td><td></td></tr><tr><td>SATA</td><td>5</td><td>原生 1 扩展 4</td></tr><tr><td>音频</td><td>1</td><td>音频输出</td></tr></tbody></table><ul><li><strong>风扇口</strong>:风扇有两个三针接口,bios 里只有 CPU-Fan 的调速选项,似乎不起作用。都说风扇特别响,但在我这听来也还好,基于成本考虑,风扇就不换了。</li><li><strong>SATA 口</strong>:mSATA 和主板上那个单独的 SATA 是原生的,可以引导系统,剩下的 4 个 SATA 是给硬盘架用的,支持热插拔。</li><li><strong>网口</strong>:原生千兆,通过 SAMBA 复制文件最高到 110MiB/s,达到千兆的速率。</li></ul><p>由上面的表格可以看出,虽然整个机器还是很渣的,但是胜在便宜。因为不要显示器,孱弱的性能同时也代表着功耗低。<br>换掉 ssd 和电源之后,不管是用作 nas、小型服务器还是 HTPC,都是足以胜任的。</p><p>下面开始拆机换 ssd 和电源。</p><h3 id="拆机"><a href="#拆机" class="headerlink" title="拆机"></a>拆机</h3><p>首先来几张偷来的外观图:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/wnxj-a-front.jpg" alt="wnxj-a-front"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/wnxj-a-drawer.jpg" alt="wnxj-a-drawer"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/wnxj-a-back.jpg" alt="wnxj-a-back"></p><p>然后就是具体的拆机过程:</p><p>拆开背后四颗快拧螺丝,打开顶盖:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_193947.jpg" alt="IMG_20190828_193947"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_193936.jpg" alt="IMG_20190828_193936"></p><p>拆下电源和 ssd:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_195629.jpg" alt="IMG_20190828_195629"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_200754.jpg" alt="IMG_20190828_200754"></p><p>将存储盘安装上硬盘抽屉:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_211604.jpg" alt="IMG_20190828_211604"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_211548.jpg" alt="IMG_20190828_211548"></p><p>安装上机壳,插上电饭煲线:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/IMG_20190828_211850.jpg" alt="IMG_20190828_211850"></p><p>下面就可以通电装系统了。</p><h3 id="装机"><a href="#装机" class="headerlink" title="装机"></a>装机</h3><p>由于价格便宜,这次一口气买了两台蜗牛星际 A 款的单网口千兆。</p><p>在某鱼上面淘了一个 4TB 的拆机硬盘,然后在 JD 上面整了个 64G 的 mSATA ,将其中一台装 Ubuntu Server 16.04。<br>将原来一些大文件备份上去,用作自己的主力仓储,和下载机器。</p><p>另外一台装了 Win10 精简版给我老娘做 HTPC。</p><h4 id="系统"><a href="#系统" class="headerlink" title="系统"></a>系统</h4><p>装系统和普通的 X86 机器一样,无需多言。</p><p>注意如果装 Linux,最好在 bios 中将启动类型设置为 Android,否则会有关机信号发出后无法的关机的尴尬情况。Win 10 则设置为 win7 即可。</p><p>Win10 装的是官方的 Win10 企业版 LTSC。</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/nas-win10.png" alt="nas-win10"></p><h4 id="软件"><a href="#软件" class="headerlink" title="软件"></a>软件</h4><ul><li><p>Linux</p><ul><li><p>安装 Docker 与面板</p><p>Docker 算是特殊的虚拟机,它可以方便的管理运行不同服务的容器。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 添加软件源,加速下载</span></span><br><span class="line">sudo add-apt-repository <span class="string">"deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu <span class="variable">$(lsb_release -cs)</span> stable"</span></span><br><span class="line">sudo apt update</span><br><span class="line"><span class="comment"># 安装 Docker-CE 等</span></span><br><span class="line">sudo apt-get install docker-ce docker-ce-cli containerd.io</span><br><span class="line"><span class="comment"># 安装装 Docker 面板 Portainer.io</span></span><br><span class="line">sudo docker volume create portainer_data</span><br><span class="line">sudo docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer</span><br></pre></td></tr></table></figure><p>之后访问 <a href="http://ip:9000" target="_blank" rel="noopener">http://ip:9000</a> ,即可登录面板管理 Docker 容器。</p></li><li><p>安装 Docker 下载容器</p><p>colinwjd 整合了一个包含 aria2+aria2Ng 的下载容器,直接安装即可。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker run --name aria2-ariang \</span><br><span class="line">-p 6800:6800 -p 6880:80 -p 6888:8080 \</span><br><span class="line">-v /实机下载目录:/aria2/downloads \</span><br><span class="line">-v /实机配置文件目录:/aria2/conf \</span><br><span class="line">-e SECRET=aria2RPC密钥 colinwjd/aria2-ariang</span><br></pre></td></tr></table></figure><p>安装完成后,访问 <a href="http://ip:6880/#!/settings/ariang" target="_blank" rel="noopener">http://ip:6880/#!/settings/ariang</a> ,在 RPC 选项卡中填入密钥。刷新页面,提示连接成功后即可开始离线下载。</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/ariaNg.png" alt="ariaNg"></p></li><li><p>Samba 服务</p><p>通过 Samba 服务可以在 windows 与 linux 之间共享文件。<br>Ubuntu Server 安装时就可以装上 Samba 服务,如果没有的话可以参考这篇文章安装:<a href="https://jingyan.baidu.com/article/3a2f7c2ed314ef26afd611a1.html" target="_blank" rel="noopener">Ubuntu 下 samba 配置和使用</a><br>安装并配置完后,在 windows 资源管理器中可以直接访问虚拟机共享的文件夹并复制或管理文件。</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/wnxj-smb.png" alt="wnxj-smb"></p></li><li><p>其他功能</p><p>好多功能都有现成的 Docker 镜像了,直接拉下来即可。如果没有,既可以自己写 Dockerfile,也可以直接在实机安装对应的软件。</p></li></ul></li><li><p>Win10</p><p>鉴于家里的光电宽带没有固定的公网 IP,做内网穿透也挺麻烦的,Win10 装了一个 TeamViewer 做远程控制。<br>登录账号后设置开机自启动也挺方便进行控制的。<br>安装 <a href="https://repo.jellyfin.org/releases/server/windows/versions/" target="_blank" rel="noopener">jellyfin</a> 来作为 HTPC 的服务器。</p><p>上几张手机端上的效果图:</p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/Screenshot_2019-08-30-09-41-44-486_org.jellyfin.mobile.png" alt="Screenshot_2019-08-30-09-41-44-486_org.jellyfin.mobile"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/Screenshot_2019-08-30-09-44-44-138_org.jellyfin.mobile.png" alt="Screenshot_2019-08-30-09-44-44-138_org.jellyfin.mobile"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/Screenshot_2019-08-30-09-40-52-057_org.jellyfin.mobile.png" alt="Screenshot_2019-08-30-09-40-52-057_org.jellyfin.mobile"></p><p><img src="https://pek3b.qingstor.com/img-cdn/blog/Screenshot_2019-08-30-09-37-48-628_org.jellyfin.mobile.png" alt="Screenshot_2019-08-30-09-37-48-628_org.jellyfin.mobile"></p><p>Windows 上的软件资源也足够丰富,无论是 BT 下载还是百度云下载等,都能轻松胜任。这边的软件安装就不多讲了,都是图形界面化的操作。</p></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>先计算下成本:</p><table><thead><tr><th>名称</th><th>价格</th><th>备注</th></tr></thead><tbody><tr><td>蜗牛星际</td><td>265 * 2</td><td>现在掉价了,囧orz</td></tr><tr><td>台达小 1u 电源</td><td>99 * 2</td><td>咸鱼上收的,250w</td></tr><tr><td>金百达 mSATA 固态</td><td>99 * 2</td><td>64G,这个是 JD 上面最便宜的 mSATA 了</td></tr><tr><td>西数 WD 4TB 黑盘</td><td>399</td><td>咸鱼上收的自用仓储盘</td></tr><tr><td>希捷 Seagate 500G</td><td>50</td><td>用于 win 上存视频</td></tr></tbody></table><p>整体下来那台 win10 的 HTPC 预算共 513 块,整个算下来还是挺有性价比的。</p><p>更换电源和 ssd 之后,经过一个多月的试用,稳定运行。</p><p>整体算下来,即使算上更换电源和 SSD 的成本,这个车还是很值得上的。当然,上车有风险,谨防被坑。</p><!-- rebuild by neat -->]]></content>
<categories>
<category> 科技随笔 </category>
</categories>
<tags>
<tag> 蜗牛星际 </tag>
<tag> nas </tag>
<tag> HTPC </tag>
<tag> 捡垃圾 </tag>
</tags>
</entry>
<entry>
<title>一周时间编写你的第二个 Flutter APP</title>
<link href="/archives/write-your-second-flutter-app.html"/>
<url>/archives/write-your-second-flutter-app.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Flutter 从发布之日起我就对其心心念念了好久。<br>奈何这段时间实在是太忙了,加之自己拖延症时不时发作下,一直都抽不出时间来学习这个跨平台框架。</p><p>一转眼 Flutter 1.2 都已经发布了,这下实在是坐不住了。特地花了一周的时间来做了 <a href="https://github.com/chengww5217/one_article" target="_blank" rel="noopener">一文</a> 这个 APP 。以此来简单了解下这款全新跨平台框架的魅力。</p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article.png" alt="one article"></p><a id="more"></a><h2 id="第一个-APP"><a href="#第一个-APP" class="headerlink" title="第一个 APP"></a>第一个 APP</h2><p>回到标题,既然是编写<strong>第二个</strong> Flutter APP,那就要求各位观众老爷自己对照着官方的 <a href="https://flutter.dev/docs/get-started/codelab" target="_blank" rel="noopener">First Flutter APP</a> 撸一遍。(中文链接:<a href="https://flutterchina.club/get-started/codelab/" target="_blank" rel="noopener">https://flutterchina.club/get-started/codelab/</a>)</p><p>万事开头难,这部分包含了 dart 语言特性的学习,和 Flutter 框架一些特性的体验。</p><p>如果你也是像我一样只是花两个小时简单看了下 dart 语言的新特性就直接来撸第一个 APP。写的时候可能会感觉总是在云里雾里,很多地方都不明白。</p><p>别担心,这是正常现象,对我们这种只想初步体验的用户来说,没有系统的学习,这种情况才是正常现象。</p><p>保持开放的心态,多学多看。碰到不懂的,多查,碰到不知道怎么写的,多看官方的源码实现,等代码量上去了,自然就熟练了。</p><h2 id="第二个-APP"><a href="#第二个-APP" class="headerlink" title="第二个 APP"></a>第二个 APP</h2><p>照着官方示例完成了自己了的第一个 APP 编写,是不是很有成就感?激动之余,似乎感觉少了点什么。</p><p>没错,毕竟只是照着官方的示例敲了一遍,说是 APP 也略简陋了。是不是迫不及待的想做点什么来巩固下自己的学习呢。</p><p>这次就跟着我一起从项目的立项开始你的第二个 APP 吧。</p><h3 id="一文"><a href="#一文" class="headerlink" title="一文"></a>一文</h3><p>由于项目一周的时间限制,本次就要求项目尽可能的简单,页面尽可能的少。且,要尽可能的完成多的功能点,于是 <a href="https://github.com/chengww5217/one_article" target="_blank" rel="noopener">一文</a> 就诞生了。</p><p><a href="https://github.com/chengww5217/one_article" target="_blank" rel="noopener">一文</a> 是基于<a href="https://meiriyiwen.com/" target="_blank" rel="noopener">每日一文 API</a> 开发的一款全新的 Flutter APP。</p><h4 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h4><p><a href="https://github.com/chengww5217/one_article/releases" target="_blank" rel="noopener">https://github.com/chengww5217/one_article/releases</a></p><h4 id="截图"><a href="#截图" class="headerlink" title="截图"></a>截图</h4><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article_screen_shot01.png" alt=""></p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article_screen_shot02.png" alt=""></p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article_screen_shot03.png" alt=""></p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article_screen_shot04.png" alt=""></p><h4 id="Highlights"><a href="#Highlights" class="headerlink" title="Highlights"></a>Highlights</h4><ul><li>这个项目足够简单<ul><li>只有 splash、home、starred list 三个界面</li></ul></li><li>这个项目功能点足够多<ul><li>splah 页面创建,去除启动白屏</li><li>联网、Json 解析、文章展示</li><li>数据库保存文章</li><li>主题切换,字体调整,配置本地保存(SP)</li><li>国际化</li><li>···</li></ul></li><li>这个项目还是有用的<ul><li>和一般纯练手 Demo 不同,<a href="https://meiriyiwen.com/" target="_blank" rel="noopener">每日一文</a>是我每天都会看的</li></ul></li></ul><h3 id="开始项目"><a href="#开始项目" class="headerlink" title="开始项目"></a>开始项目</h3><p>注意:在源代码中,一个页面可能包含多个功能实现,在实际做的时候,请依据 APP 预览一项一项进行实现。</p><p>源代码并不是标准答案,有问题欢迎提交 PR 进行贡献。</p><h4 id="API"><a href="#API" class="headerlink" title="API"></a>API</h4><p>API 来源:<a href="https://github.com/jokermonn/-Api/blob/master/OneArticle.md" target="_blank" rel="noopener">https://github.com/jokermonn/-Api/blob/master/OneArticle.md</a></p><p>分析 API 进行联网实现,主要要求实现联网获取文章,获取后进行 Json 解析到 bean,然后进行简单的错误 handle。</p><p>参考文章:</p><ul><li>英文:<a href="https://flutter.dev/docs/cookbook/networking/fetch-data" target="_blank" rel="noopener">https://flutter.dev/docs/cookbook/networking/fetch-data</a></li><li>中文:<a href="https://flutterchina.club/networking/" target="_blank" rel="noopener">https://flutterchina.club/networking/</a></li></ul><h4 id="Splash-Page"><a href="#Splash-Page" class="headerlink" title="Splash Page"></a>Splash Page</h4><p>按照原生 APP 开发的套路,用于初始化以及展示广告的 Splash 是必不可少的。</p><p>第一个页面,主要要求掌握页面编写的常规套路以及 StatefulWidget 的生命周期等。</p><p>比如 class <code>SplashPage</code> 的写法:</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SplashPage</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>{</span><br><span class="line"> SplashPage({Key key, <span class="keyword">this</span>.title}) : <span class="keyword">super</span>(key: key);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">String</span> title;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> State<StatefulWidget> createState() {</span><br><span class="line"> <span class="keyword">return</span> _SplashPageState();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后就是 class <code>_SplashPageState</code> 的编写。</p><p>布局就是一张图片:</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">'package:flutter/material.dart'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SplashPage</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>{</span><br><span class="line"> SplashPage({Key key, <span class="keyword">this</span>.title}) : <span class="keyword">super</span>(key: key);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">String</span> title;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> State<StatefulWidget> createState() {</span><br><span class="line"> <span class="keyword">return</span> _SplashPageState();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">_SplashPageState</span> <span class="keyword">extends</span> <span class="title">State</span><<span class="title">SplashPage</span>> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> <span class="keyword">void</span> initState() {</span><br><span class="line"> <span class="comment">// <span class="doctag">TODO:</span> do something to init</span></span><br><span class="line"> <span class="keyword">super</span>.initState();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> Widget build(BuildContext context) {</span><br><span class="line"> <span class="keyword">return</span> Builder(builder: (context) {</span><br><span class="line"> <span class="keyword">return</span> Container(</span><br><span class="line"> child: Image(image: AssetImage(<span class="string">'assets/images/splash.png'</span>), fit: BoxFit.fill,),</span><br><span class="line"> );</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>生命周期如下:</p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/flutter_state_life_cicle.png" alt=""></p><p>图片转载自<a href="https://segmentfault.com/a/1190000015211309" target="_blank" rel="noopener">https://segmentfault.com/a/1190000015211309</a></p><p>具体 Splash 页面讲解参考我的博客:<a href="https://chengww.com/archives/flutter-splash.html">Flutter 开发 Android & IOS 启动页 splash page</a></p><h4 id="Home-Page"><a href="#Home-Page" class="headerlink" title="Home Page"></a>Home Page</h4><p>主页面主要是对文章进行展示以及相关设置项。</p><p><img src="https://img-cdn.pek3b.qingstor.com/write-your-second-flutter-app/one_article_settings.png" alt=""></p><p>点击左上角可以弹出相关配置弹窗。</p><p>考虑到布局的复杂度,这里可以将底部弹窗抽离出单独写进一个 <code>.dart</code> 文件。</p><p>从这部分就涉及到各个控件的使用和状态设置,点击事件等内容。</p><p>这部分基本就是程序的核心内容了。</p><hr><p>完成了该部分之后就是对程序进行优化,添加数据库来缓存数据,日期判断切换,文章收藏等。</p><p>这个部分是耗时最长的,也是从磕磕碰碰到逐渐熟练的过程。</p><h4 id="杂项"><a href="#杂项" class="headerlink" title="杂项"></a>杂项</h4><p>最后就是国际化,添加资源和打包等一些杂项了,具体参见</p><p><a href="https://flutter.dev/docs/development/accessibility-and-localization/internationalization" target="_blank" rel="noopener">https://flutter.dev/docs/development/accessibility-and-localization/internationalization</a></p><p><a href="https://flutter.dev/docs/deployment/android" target="_blank" rel="noopener">https://flutter.dev/docs/deployment/android</a></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>从刚开始看 dart 语法,到这个项目开发完成。断断续续一共持续了三周的时间。每天抽出一到两个小时,合计一共是 56 小时左右。减去画 APP 图标,启动页面图片的两个小时,勉强算得上八小时工作制的一周。</p><p>这一周的使用过程中,Flutter 有些特性让人感觉相见恨晚:语法特性(类型动态检查,支持 <code>.?</code> <code>??</code> 操作符)、简单方便完备的 UI 方案、Hot Reload等。但是诸如复杂冗长的 view tree、资源的硬编码、糟糕的 UI 控件 API 等又让人头痛不已。</p><p>在初步使用 Flutter 之后,我发觉似乎 Flutter 短时间内并不能让我不学习原生开发就直接使用 Flutter 解决移动客户端开发。</p><p>在不断的使(zhe)用(teng)过程中,发现碰到好多问题还是需要你必须用原生开发的知识去解决相应的问题。 比如项目里面获取已收藏文章,是这样从数据库里面获取文件的:</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line">Future<<span class="built_in">List</span><ArticleBean>> getStarred() <span class="keyword">async</span> {</span><br><span class="line"> <span class="built_in">List</span><ArticleBean> articles = <span class="built_in">List</span>();</span><br><span class="line"> Database db = <span class="keyword">await</span> getDB();</span><br><span class="line"> <span class="built_in">List</span><<span class="built_in">Map</span><<span class="built_in">String</span>, <span class="keyword">dynamic</span>>> maps =</span><br><span class="line"> <span class="keyword">await</span> db.query(name, columns: [columnId, columnStarred, columnDate, columnData], where: <span class="string">"$columnStarred = ?"</span>, whereArgs: [<span class="keyword">true</span>]);</span><br><span class="line"> <span class="keyword">if</span> (maps.length > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">Map</span><<span class="built_in">String</span>, <span class="keyword">dynamic</span>> map <span class="keyword">in</span> maps) {</span><br><span class="line"> ArticleBean article = ArticleBean.fromJson(map);</span><br><span class="line"> articles.add(article);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> articles;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>请看核心查询代码</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> db.query(name, columns: [columnId, columnStarred, columnDate, columnData], where: <span class="string">"$columnStarred = ?"</span>, whereArgs: [<span class="keyword">true</span>]);</span><br></pre></td></tr></table></figure><p>这里就是获取所有 starred 为 true 的列。</p><p>但是实际运行的时候,发现在 Android 设备上面总是获取不到。</p><p>Android 数据库使用的是 Sqlite,不能存储 bool(boolean)。相反,布尔值被存储为整数 0(false)和 1(true)。</p><p>故上述代码要改成下面的代码才生效。</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">await</span> db.query(name, columns: [columnId, columnStarred, columnDate, columnData], where: <span class="string">"$columnStarred > ?"</span>, whereArgs: [<span class="number">0</span>]);</span><br></pre></td></tr></table></figure><p>这只是一个很小的简单例子,但是也说明了在 Flutter 上面并不能总是帮你解决原生的一些坑(这其实取决于各个框架的开发者为你做了多少兼容处理)。</p><hr><p>本文到这里就要结束了,不怎么涉及具体代码,只是一周时间的 Flutter 简单上手。如果大家对项目有兴趣,欢迎大家 star、fork 以及提交 PR,谢谢大家。</p><p>项目地址:<a href="https://github.com/chengww5217/one_article" target="_blank" rel="noopener">https://github.com/chengww5217/one_article</a></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Flutter </category>
</categories>
<tags>
<tag> Flutter </tag>
<tag> dart </tag>
<tag> Flutter APP </tag>
</tags>
</entry>
<entry>
<title>发布构件到 Maven 中央仓库遇到的坑</title>
<link href="/archives/problems-in-central-maven.html"/>
<url>/archives/problems-in-central-maven.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>作为 Java 开发者(伪),工作中一定离不开 Maven。<br>偶尔也需要发布自己的构件到 Maven 中央仓库中(<a href="https://oss.sonatype.org/" target="_blank" rel="noopener">https://oss.sonatype.org/</a>)。<br>但是经常有这样那样的坑(因为总是换电脑 XD),在这里记录一下,以备后续查阅。</p><p><img src="https://img-cdn.pek3b.qingstor.com/problems-in-central-maven/maven-logo.jpg" alt="Maven Logo"></p><a id="more"></a><h2 id="上传步骤"><a href="#上传步骤" class="headerlink" title="上传步骤"></a>上传步骤</h2><p>将项目发布到 maven 中央仓库的一般步骤如下:</p><ol><li><p>注册Sonatype的账户。地址:<a href="https://issues.sonatype.org/secure/Signup!default.jspa" target="_blank" rel="noopener">https://issues.sonatype.org/secure/Signup!default.jspa</a></p></li><li><p>提交发布申请。(仅第一次)</p><ul><li>创建 Issue:<a href="https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134" target="_blank" rel="noopener">https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134</a></li><li>项目类型是 <code>Community Support - Open Source Project Repository Hosting</code></li><li><code>groupId</code> 对应的域名你需要有所有权</li></ul></li><li><p>使用 GPG 生成密钥对。</p><p>Windows 安装:<a href="http://gpg4win.org/" target="_blank" rel="noopener">http://gpg4win.org/</a></p><p>Mac 安装:<code>brew install gpg</code></p><ul><li><code>gpg --version</code> 检查是否安装成功</li><li><code>gpg --gen-key</code> 生成密钥对</li><li><code>gpg --list-keys</code> 查看公钥</li><li><code>gpg --keyserver hkp://pool.sks-keyservers.net --send-keys 公钥ID</code> 将公钥发布到 PGP 密钥服务器</li><li><code>gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 公钥ID</code> 查询公钥是否发布成功</li></ul></li><li><p>配置 maven。<br>需要修改的 Maven 配置文件包括:<code>setting.xml</code>(全局级别)与 <code>pom.xml</code>(项目级别)。</p><ul><li><p><code>setting.xml</code> 配置一览</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">settings</span>></span></span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">servers</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">server</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>snapshotRepository-id<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">username</span>></span>用户名<span class="tag"></<span class="name">username</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">password</span>></span>密码<span class="tag"></<span class="name">password</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">server</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">servers</span>></span></span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">settings</span>></span></span><br></pre></td></tr></table></figure><p>使用自己注册的 Sonatype 账号的用户名与密码来配置以上 server 信息。</p><p>此处 id <code>snapshotRepository-id</code> 应和下面 <code>pom.xml</code> 中 snapshotRepository 和 repository 里面的 id 保持一致。</p><ul><li><code>pom.xml</code> 配置一览<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">project</span>></span></span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">name</span>></span>your project's name<span class="tag"></<span class="name">name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">description</span>></span>your project's description<span class="tag"></<span class="name">description</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>http://www.chengww.com/<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">licenses</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">license</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">name</span>></span>The Apache Software License, Version 2.0<span class="tag"></<span class="name">name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>http://www.apache.org/licenses/LICENSE-2.0.txt<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">license</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">licenses</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">developers</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">developer</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">name</span>></span>chengww<span class="tag"></<span class="name">name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">email</span>></span>[email protected]<span class="tag"></<span class="name">email</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">developer</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">developers</span>></span></span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">profiles</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">profile</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>release<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">build</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">plugins</span>></span></span><br><span class="line"> <span class="comment"><!-- Source --></span></span><br><span class="line"> <span class="tag"><<span class="name">plugin</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.apache.maven.plugins<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>maven-source-plugin<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.2.1<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">phase</span>></span>package<span class="tag"></<span class="name">phase</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goal</span>></span>jar-no-fork<span class="tag"></<span class="name">goal</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">plugin</span>></span></span><br><span class="line"> <span class="comment"><!-- Javadoc --></span></span><br><span class="line"> <span class="tag"><<span class="name">plugin</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.apache.maven.plugins<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>maven-javadoc-plugin<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.9.1<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">phase</span>></span>package<span class="tag"></<span class="name">phase</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goal</span>></span>jar<span class="tag"></<span class="name">goal</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">plugin</span>></span></span><br><span class="line"> <span class="comment"><!-- GPG --></span></span><br><span class="line"> <span class="tag"><<span class="name">plugin</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.apache.maven.plugins<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>maven-gpg-plugin<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>1.5<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">phase</span>></span>verify<span class="tag"></<span class="name">phase</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">goal</span>></span>sign<span class="tag"></<span class="name">goal</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">goals</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">execution</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">executions</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">plugin</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">plugins</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">build</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">distributionManagement</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">snapshotRepository</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>snapshotRepository-id<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>https://oss.sonatype.org/content/repositories/snapshots/<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">snapshotRepository</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">repository</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>snapshotRepository-id<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url</span>></span>https://oss.sonatype.org/service/local/staging/deploy/maven2/<span class="tag"></<span class="name">url</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">repository</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">distributionManagement</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">profile</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">profiles</span>></span></span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="tag"></<span class="name">project</span>></span></span><br></pre></td></tr></table></figure></li></ul><p>注意:以上 <code>pom.xml</code> 必须包括:name、description、url、licenses、developers、scm 等基本信息,此外,使用了 Maven 的 profile 功能,只有在 release 的时候,创建源码包、创建文档包、使用 GPG 进行数字签名。此外,snapshotRepository 与 repository 中的 id 一定要与 <code>setting.xml</code> 中 server 的 id 保持一致。</p></li></ul></li><li><p>上传构件到 OSS 中。<code>mvn clean deploy -P release</code></p></li><li><p>在 OSS 中发布构件。进入 <a href="https://oss.sonatype.org/" target="_blank" rel="noopener">https://oss.sonatype.org/</a>,点击<code>Staging Repositories</code> -> 在搜索栏输入你的 groupId -> 勾选你的构件并点击 close -> 点击 tab 栏的 release。</p></li><li><p>通知 Sonatype 的工作人员关闭 issue。(仅第一次)</p></li></ol><p>参考地址:<a href="https://my.oschina.net/huangyong/blog/226738" target="_blank" rel="noopener">https://my.oschina.net/huangyong/blog/226738</a></p><hr><p>等待审批通过后,就可以在中央仓库中搜索到自己发布的构件了!</p><p>但是,事情并不是那么简单的。总是会出现这样那样的坑。</p><h2 id="碰到的坑一览"><a href="#碰到的坑一览" class="headerlink" title="碰到的坑一览"></a>碰到的坑一览</h2><h3 id="GPG-生成密钥对"><a href="#GPG-生成密钥对" class="headerlink" title="GPG 生成密钥对"></a>GPG 生成密钥对</h3><h4 id="消息提示乱码"><a href="#消息提示乱码" class="headerlink" title="消息提示乱码"></a>消息提示乱码</h4><p><img src="https://img-cdn.pek3b.qingstor.com/problems-in-central-maven/gpg-password-check.png" alt=""></p><p>出现该步骤其实是输入密码的步骤。但是由于是中文的缘故,消息提示乱码了。</p><p>只需要在下面横线上输入密码之后,将光标移动到下面的好,回车即可。注意,密码需要输入两次,请保持两次一致。</p><h3 id="将公钥发布到-PGP-密钥服务器"><a href="#将公钥发布到-PGP-密钥服务器" class="headerlink" title="将公钥发布到 PGP 密钥服务器"></a>将公钥发布到 PGP 密钥服务器</h3><h4 id="gpg-发送至公钥服务器失败:Server-indicated-a-failure"><a href="#gpg-发送至公钥服务器失败:Server-indicated-a-failure" class="headerlink" title="gpg: 发送至公钥服务器失败:Server indicated a failure"></a>gpg: 发送至公钥服务器失败:Server indicated a failure</h4><p>因安装了新版的 gpg,在 <code>gpg --list-keys</code> 时显示如下:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pub rsa2048 2019-04-12 [SC] [有效至:2021-04-11]</span><br><span class="line"> 9A1640F7A2551131612D51B12D83594B7B29D86A</span><br><span class="line">uid [ 绝对 ] chengww <[email protected]></span><br><span class="line">sub rsa2048 2019-04-12 [E] [有效至:2021-04-11]</span><br></pre></td></tr></table></figure><p>发布公钥到服务器时,填的公钥 ID 为 <code>9A1640F7A2551131612D51B12D83594B7B29D86A</code>,终端上显示为:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gpg --keyserver hkp://subkeys.pgp.net --send-keys 9A1640F7A2551131612D51B12D83594B7B29D86A</span><br><span class="line">...</span><br><span class="line">gpg: 正在发送密钥 2D83594B7B29D86A 到 hkp://subkeys.pgp.net</span><br><span class="line">gpg: 发送至公钥服务器失败:Server indicated a failure</span><br><span class="line">gpg: 发送至公钥服务器失败:Server indicated a failure</span><br></pre></td></tr></table></figure><p>只需要将公钥 ID 从 <code>9A1640F7A2551131612D51B12D83594B7B29D86A</code> 修改成 <code>2D83594B7B29D86A</code> 即可:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gpg --keyserver hkp://subkeys.pgp.net --send-keys 2D83594B7B29D86A</span><br></pre></td></tr></table></figure><h3 id="配置-maven"><a href="#配置-maven" class="headerlink" title="配置 maven"></a>配置 maven</h3><h4 id="全局级别-setting-xml-在哪里配置"><a href="#全局级别-setting-xml-在哪里配置" class="headerlink" title="全局级别 setting.xml 在哪里配置"></a>全局级别 setting.xml 在哪里配置</h4><p><code>settings.xml</code> 文件一般存在于两个位置:<br>全局配置: <code>${M2_HOME}/conf/settings.xml</code><br>用户配置:<code>${user.home}/.m2/settings.xml</code></p><p>如果实在是不清楚的,请自行 <code>mvn -X</code> 查看:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">...</span><br><span class="line">[DEBUG] Reading global settings from /usr/<span class="built_in">local</span>/Cellar/maven/3.6.0/libexec/conf/settings.xml</span><br><span class="line">[DEBUG] Reading user settings from /Users/chengww/.m2/settings.xml</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>关于 <code>setting.xml</code> 相关讲解参见:<a href="https://www.jianshu.com/p/110d897a5442" target="_blank" rel="noopener">https://www.jianshu.com/p/110d897a5442</a></p><h3 id="上传构件到-OSS"><a href="#上传构件到-OSS" class="headerlink" title="上传构件到 OSS"></a>上传构件到 OSS</h3><h4 id="Maven-Sonatype-Nexus-return-401"><a href="#Maven-Sonatype-Nexus-return-401" class="headerlink" title="Maven Sonatype Nexus return 401"></a>Maven Sonatype Nexus return 401</h4><p>401 错误,一般都是未在 <code>setting.xml</code> 中设置用户名密码所致(或用户名密码不正确)。</p><p>参见上述 <strong>4.配置 maven</strong> 配置下 <code>setting.xml</code>。</p><h4 id="gpg-signing-failed-Inappropriate-ioctl-for-device"><a href="#gpg-signing-failed-Inappropriate-ioctl-for-device" class="headerlink" title="gpg: signing failed: Inappropriate ioctl for device"></a>gpg: signing failed: Inappropriate ioctl for device</h4><p>原因是新版 gpg 在当前终端无法弹出密码输入页面。</p><p>解决:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> GPG_TTY=$(tty)</span><br></pre></td></tr></table></figure><p>在当前终端中 export(临时解决)</p><p>或者加入到 <code>~/.bash_profile</code>,然后 <code>source ~/.bash_profile</code></p><h4 id="gpg-signing-failed-Screen-or-window-too-small"><a href="#gpg-signing-failed-Screen-or-window-too-small" class="headerlink" title="gpg: signing failed: Screen or window too small"></a>gpg: signing failed: Screen or window too small</h4><p>执行上述命令后在 IntelliJ IDEA 中的终端(Terminal)中还是不能弹出密码输入界面,且报上面的错。</p><p>这个时候就要到系统的的终端,cd 到项目目录,然后执行 <code>mvn clean deploy -P release</code></p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>路漫漫其修远兮,愿天下没有 BUG。</p><p><img src="https://img-cdn.pek3b.qingstor.com/problems-in-central-maven/bug-written.jpg" alt=""></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Maven 中央仓库 </tag>
<tag> Maven </tag>
<tag> Sonatype </tag>
</tags>
</entry>
<entry>
<title>Flutter 开发 Android & IOS 启动页 splash page</title>
<link href="/archives/flutter-splash.html"/>
<url>/archives/flutter-splash.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>Hello,好久不见呀。最近对 <a href="https://flutter.dev/" target="_blank" rel="noopener">Flutter</a> 比较感兴趣,一直都在在看 <a href="https://flutter.dev/" target="_blank" rel="noopener">Flutter</a> 相关的内容。</p><p>准备简单的做一个 Flutter 的项目,也是好久没有更新博客了,正好结合里面启动页相关的内容写一篇博客。</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/flutter-logo.jpg" alt="Flutter"></p><a id="more"></a><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>启动页面(Splash)对于一个 APP 来说还是挺重要的,不设置启动页面打开 APP(特别是冷启动)时会有很长时间的白屏效果,这个对于用户体验来说,非常不友好。</p><p>OKay,下面开始进入启动页面的撰写。</p><h3 id="Flutter-页面"><a href="#Flutter-页面" class="headerlink" title="Flutter 页面"></a>Flutter 页面</h3><h4 id="资源引入"><a href="#资源引入" class="headerlink" title="资源引入"></a>资源引入</h4><p>首先将启动页面的图片加入到项目目录:<code>assets/images/splash.png</code>,这里支持多分辨率图片,比如有 <code>@3x</code> 的图片可以放进 <code>assets/images/3.0x/splash.png</code>。这里的 <code>@3x</code> 和 IOS 是一样的。</p><p><em>注:IOS @3x 渲染后的分辨率为 <code>1080x1920</code>,等于 Android 的 xxhdpi</em></p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/flutter-resources.png" alt="资源文件"></p><p>然后在 <code>pubspec.yaml</code> 文件中引入资源:</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">flutter:</span></span><br><span class="line"><span class="attr"> assets:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">assets/images/splash.png</span></span><br></pre></td></tr></table></figure><h4 id="页面创建"><a href="#页面创建" class="headerlink" title="页面创建"></a>页面创建</h4><p>启动页面首先也是一个页面,命名为 <code>splash_page.dart</code>。</p><figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">'package:flutter/material.dart'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SplashPage</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>{</span><br><span class="line"> SplashPage({Key key, <span class="keyword">this</span>.title}) : <span class="keyword">super</span>(key: key);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">String</span> title;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> State<StatefulWidget> createState() {</span><br><span class="line"> <span class="keyword">return</span> _SplashPageState();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">_SplashPageState</span> <span class="keyword">extends</span> <span class="title">State</span><<span class="title">SplashPage</span>> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> <span class="keyword">void</span> initState() {</span><br><span class="line"> <span class="comment">// <span class="doctag">TODO:</span> do something to init</span></span><br><span class="line"> <span class="keyword">super</span>.initState();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> Widget build(BuildContext context) {</span><br><span class="line"> <span class="keyword">return</span> Builder(builder: (context) {</span><br><span class="line"> <span class="keyword">return</span> Container(</span><br><span class="line"> child: Image(image: AssetImage(<span class="string">'assets/images/splash.png'</span>), fit: BoxFit.fill,),</span><br><span class="line"> );</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>代码很简单,就是一张图片,然后全屏填充。这里 fit 全屏的方式有两个选择:</p><ul><li><code>BoxFit.fill</code><br>以(上下左右)拉伸的方式充满屏幕,不裁剪原图;<br>对应 IOS Content Mode:<code>Scale to fill</code>;<br>对应 Android xml 标签 <code><bitmap></code> 内属性 gravity:<code>fill</code>。</li><li><code>BoxFit.cover</code><br>以裁剪的方式充满屏幕<br>对应 IOS Content Mode:<code>Aspect fill</code>。</li></ul><p>考虑到 Android 启动页面设置的全屏模式,这里选择 <code>BoxFit.fill</code>。</p><p>完成后运行程序会发现还是会有短暂的白屏时间,这是因为程序启动时加载所致。现在就需要我们在原生项目中添加启动页面背景。</p><h3 id="Android-启动背景"><a href="#Android-启动背景" class="headerlink" title="Android 启动背景"></a>Android 启动背景</h3><p>将 <code>splash.png</code> 按分辨率添加到对应的目录 <code>mipmap-xhdpi/mipmap-xxhdpi/mipmap-xxxhdpi</code>。<br><code>mipmap-xxhdpi</code> 对应的分辨率是 <code>1080x1920</code>。</p><p>然后打开项目的 <code>./android/app/src/main/res/drawable/launch_background.xml</code> 文件,添加如下代码:</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?><span class="comment"><!-- Modify this file to customize your launch splash screen --></span></span><br><span class="line"><span class="tag"><<span class="name">layer-list</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span>></span></span><br><span class="line"> <span class="comment"><!--<item android:drawable="@color/blue" />--></span></span><br><span class="line"></span><br><span class="line"> <span class="comment"><!-- You can insert your own image assets here --></span></span><br><span class="line"> <span class="tag"><<span class="name">item</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bitmap</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:gravity</span>=<span class="string">"fill"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:mipMap</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:src</span>=<span class="string">"@mipmap/splash"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">item</span>></span></span><br><span class="line"><span class="tag"></<span class="name">layer-list</span>></span></span><br></pre></td></tr></table></figure><hr><p>layer-list 其实就是将多个 drawable 按照顺序层叠在一起显示,在最前面的比如上面被注释掉的</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">item</span> <span class="attr">android:drawable</span>=<span class="string">"@color/blue"</span> /></span></span><br></pre></td></tr></table></figure><p>会显示在最底层,成为背景。这里我们只想设置图片,直接注释掉。</p><p>gravity 为 fill 会将图片拉伸充满屏幕,和我们之后出现的页面 <code>splash_page.dart</code> 里面的图片 <code>fit: BoxFit.fill</code> 保持一致。</p><p>此时 Android 程序启动时就没有白屏了。</p><h3 id="IOS-启动背景"><a href="#IOS-启动背景" class="headerlink" title="IOS 启动背景"></a>IOS 启动背景</h3><ol><li><p>使用 xcode 打开项目的 <code>ios</code> 目录。</p></li><li><p>拖拽图片进 xcode 项目打开界面左侧 <code>Runner</code> 根目录。</p><p>勾选 <code>Copy items if needed</code>,选中 <code>Create groups</code> 并在下方勾选 <code>Runner</code>。</p><p>如图:</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/copy-items-if-needed.png" alt="copy-items-if-needed"></p></li><li><p>完成后在左侧 <code>Project navigator</code> 打开文件 <code>Runner/Runner/LaunchScreen.storyboard</code>。</p><p>然后在中间部分点开 <code>view tree</code>,找到 <code>LaunchImage</code>。 如图:</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/launch-image.png" alt="LaunchImage"></p><p>点击后查看右侧 <code>Attributes inspector</code>,在 <code>image</code> 一栏中填写 <code>splash.png</code>,并将 <code>Content Mode</code> 修改为 <code>Scale To Fill</code>:</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/splash-view-tree.png" alt="splash-view-tree"></p></li><li><p>选中图片,然后在左侧 <code>View Controller scence</code> 中选中并剪切该图片 <code>splash.png</code> 并粘贴,以清除十字线(约束)。</p><p>编辑图片的约束,使其充满全屏幕。</p><p>点击屏幕右下角的约束编辑器:</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/splash-masonry.png" alt="splash-masonry"></p><p>将上面填空处都填 0,然后点击 <code>Add 4 Constraints</code>。</p></li><li><p>现在运行 Flutter 项目到 IOS 设备可以发现启动时的白屏已经没有了。</p></li></ol><h3 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h3><p>最后附下实际效果:</p><p><img src="https://img-cdn.pek3b.qingstor.com/flutter-splash/splash-ios-demo.gif" alt="splash-ios-demo"></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Flutter </category>
</categories>
<tags>
<tag> Flutter </tag>
<tag> dart </tag>
<tag> splash page </tag>
</tags>
</entry>
<entry>
<title>从零开始为 PicGo 开发一个新图床</title>
<link href="/archives/picgo-plugin-uploader-development.html"/>
<url>/archives/picgo-plugin-uploader-development.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>用过几款上传图片到图床的软件,但是自己常用的图床,比如<a href="https://www.qingcloud.com/products/qingstor/" target="_blank" rel="noopener">青云对象存储</a>基本都没有支持的。</p><p>刚好前几天发现了一款可以自定义插件的图片上传软件 <a href="https://molunerfinn.com/PicGo/" target="_blank" rel="noopener">PicGo</a>,借此机会正好为其新增<a href="https://www.qingcloud.com/products/qingstor/" target="_blank" rel="noopener">青云对象存储</a>图床的支持。</p><p><img src="https://img-cdn.pek3b.qingstor.com/picgo-plugin-uploader-development/picgo-qingstor-uploader-configuration.png" alt="picgo-qingstor-uploader-configuration.png"></p><p>项目地址:<a href="https://github.com/chengww5217/picgo-plugin-qingstor-uploader" target="_blank" rel="noopener">picgo-plugin-qingstor-uploader</a></p><a id="more"></a><h3 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h3><p>插件基于 <a href="https://github.com/PicGo/PicGo-Core" target="_blank" rel="noopener">PicGo-Core</a> 开发,参阅开发文档 <a href="https://picgo.github.io/PicGo-Core-Doc/" target="_blank" rel="noopener">PicGo-Core-Doc</a> 进行开发。</p><ol><li><p>确保已安装 <a href="https://nodejs.org/en/" target="_blank" rel="noopener">Node.js</a> 版本 >= 8</p></li><li><p>全局安装</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">yarn global add picgo <span class="comment"># 或者 npm install picgo -g</span></span><br></pre></td></tr></table></figure></li><li><p>使用插件模板</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">picgo init plugin <your-project-name></span><br></pre></td></tr></table></figure><ul><li>所有插件以 <code>picgo-plugin-xxx</code> 的方式命名</li><li>按照提示配置你的项目</li></ul></li></ol><h3 id="开发插件"><a href="#开发插件" class="headerlink" title="开发插件"></a>开发插件</h3><blockquote><p>picgo 是个上传的流程系统。因此插件其实就是针对这个流程系统的某个部件或者某些部件的开发。</p></blockquote><p>附一下流程图:</p><p><img src="https://img-cdn.pek3b.qingstor.com/picgo-plugin-uploader-development/picgo-core-fix.jpg" alt="picgo-core-fix.jpg"></p><blockquote><p>其中可以供开发的部件总共有5个:</p><p>两个模块:</p><ol><li>Transformer</li><li>Uploader</li></ol><p>三个生命周期插件入口:</p><ol><li>beforeTransformPlugins</li><li>beforeUploadPlugins</li><li>afterUploadPlugins</li></ol><p>通常来说如果你只是要实现一个 picgo 默认不支持的图床的话,你只需要开发一个 <code>Uploader</code> 。</p></blockquote><p>我们这里只是开发图床的话就只需要开发 <code>Uploader</code> 即可。</p><hr><p>这里定位到项目的 <code>src/index.ts</code> 或 <code>src/index.js</code> ,</p><p>在这里就是你所要支持图床的配置的地方了。</p><h4 id="图床配置文件"><a href="#图床配置文件" class="headerlink" title="图床配置文件"></a>图床配置文件</h4><p>添加必须的配置项,新增图床配置:</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> { PluginConfig } <span class="keyword">from</span> <span class="string">'picgo/dist/utils/interfaces'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> config = (ctx: picgo): PluginConfig[] => {</span><br><span class="line"> <span class="keyword">let</span> userConfig = ctx.getConfig(<span class="string">'picBed.qingstor-uploader'</span>)</span><br><span class="line"> <span class="keyword">if</span> (!userConfig) {</span><br><span class="line"> userConfig = {}</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> config = [</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'accessKeyId'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'input'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.accessKeyId || <span class="string">''</span>,</span><br><span class="line"> message: <span class="string">'AccessKeyId 不能为空'</span>,</span><br><span class="line"> required: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'accessKeySecret'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'password'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.accessKeySecret || <span class="string">''</span>,</span><br><span class="line"> message: <span class="string">'AccessKeySecret 不能为空'</span>,</span><br><span class="line"> required: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'bucket'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'input'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.bucket || <span class="string">''</span>,</span><br><span class="line"> message: <span class="string">'Bucket不能为空'</span>,</span><br><span class="line"> required: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'zone'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'input'</span>,</span><br><span class="line"> alias: <span class="string">'区域'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.area || <span class="string">''</span>,</span><br><span class="line"> message: <span class="string">'区域代码不能为空'</span>,</span><br><span class="line"> required: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'path'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'input'</span>,</span><br><span class="line"> alias: <span class="string">'存储路径'</span>,</span><br><span class="line"> message: <span class="string">'blog'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.path || <span class="string">''</span>,</span><br><span class="line"> required: <span class="literal">false</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> name: <span class="string">'customUrl'</span>,</span><br><span class="line"> <span class="keyword">type</span>: <span class="string">'input'</span>,</span><br><span class="line"> alias: <span class="string">'私有云网址'</span>,</span><br><span class="line"> message: <span class="string">'https://qingstor.com'</span>,</span><br><span class="line"> <span class="keyword">default</span>: userConfig.customUrl || <span class="string">''</span>,</span><br><span class="line"> required: <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> <span class="keyword">return</span> config</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="签名配置"><a href="#签名配置" class="headerlink" title="签名配置"></a>签名配置</h4><p>根据<a href="https://docs.qingcloud.com/qingstor/api/common/signature.html#%E6%9E%84%E5%BB%BA%E7%AD%BE%E5%90%8D%E4%B8%B2" target="_blank" rel="noopener">青云对象存储签名</a>特点,使用 accessKeyId 和 accessKeySecret 生成上传时的签名。</p><ol><li><p>首先观察 <code>strToSign</code> :</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">strToSign = Verb + <span class="string">"\n"</span></span><br><span class="line"> + Content-MD5 + <span class="string">"\n"</span></span><br><span class="line"> + Content-Type + <span class="string">"\n"</span></span><br><span class="line"> + Date + <span class="string">"\n"</span></span><br><span class="line"> (+ Canonicalized Headers + <span class="string">"\n"</span>)</span><br><span class="line"> + Canonicalized Resource</span><br></pre></td></tr></table></figure><p>这里只上传图片,<code>Verb</code> 就是 <code>PUT</code> ,<code>Date</code> 使用 <code>new Date().toUTCString()</code> 。</p><p>考虑到签名的复杂程度,上传时不发送 Content-MD5 和 Content-Type 请求头以降低签名方法的复杂度。</p></li><li><p>然后就是 <code>Canonicalized Headers</code> :</p><blockquote><p>Canonicalized Headers 代表请求头中以 x-qs- 开头的字段。如果该值为空,不保留空白行</p></blockquote><p>这种自定义的请求头肯定是没有的,也可以去掉。</p></li><li><p>Canonicalized Resource 代表请求访问的资源</p><p>默认形式:<code>/bucketName/path/fileName</code></p><p>考虑到 <code>path</code> 和 <code>fileName</code> 可能的中文情况,需要对其 encode 一下。</p></li><li><p>对 <code>strToSign</code> 进行签名</p><p>将API密钥的私钥 (<code>accessKeySecret</code>) 作为 key,使用 <code>Hmac sha256</code> 算法给签名串生成签名, 然后将签名进行 Base64 编码,最后拼接签名。</p></li></ol><p>完整代码如下:</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> crypto <span class="keyword">from</span> <span class="string">'crypto'</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// generate QingStor signature</span></span><br><span class="line"><span class="keyword">const</span> generateSignature = (options: <span class="built_in">any</span>, fileName: <span class="built_in">string</span>): <span class="function"><span class="params">string</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> date = <span class="keyword">new</span> <span class="built_in">Date</span>().toUTCString()</span><br><span class="line"> <span class="keyword">const</span> strToSign = <span class="string">`PUT\n\n\n<span class="subst">${date}</span>\n/<span class="subst">${options.bucket}</span>/<span class="subst">${encodeURI(options.path)}</span>/<span class="subst">${encodeURI(fileName)}</span>`</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> signature = crypto.createHmac(<span class="string">'sha256'</span>, options.accessKeySecret).update(strToSign).digest(<span class="string">'base64'</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`QS <span class="subst">${options.accessKeyId}</span>:<span class="subst">${signature}</span>`</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="protocol-和-host"><a href="#protocol-和-host" class="headerlink" title="protocol 和 host"></a>protocol 和 host</h4><p>对于配置了 <code>customUrl</code> 的私有云用户,需要获取到 <code>protocol</code> 和 <code>host</code> 。</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> getHost = (customUrl: <span class="built_in">any</span>): <span class="function"><span class="params">any</span> =></span> {</span><br><span class="line"> <span class="keyword">let</span> protocol = <span class="string">'https'</span></span><br><span class="line"> <span class="keyword">let</span> host = <span class="string">'qingstor.com'</span></span><br><span class="line"> <span class="keyword">if</span> (customUrl) {</span><br><span class="line"> <span class="keyword">if</span> (customUrl.startsWith(<span class="string">'http://'</span>)) {</span><br><span class="line"> protocol = <span class="string">'http'</span></span><br><span class="line"> host = customUrl.substring(<span class="number">7</span>)</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (customUrl.startsWith(<span class="string">'https://'</span>)) {</span><br><span class="line"> host = customUrl.substring(<span class="number">8</span>)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> host = customUrl</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> protocol: protocol,</span><br><span class="line"> host: host</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="配置-request"><a href="#配置-request" class="headerlink" title="配置 request"></a>配置 request</h4><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> postOptions = (options: <span class="built_in">any</span>, fileName: <span class="built_in">string</span>, signature: <span class="built_in">string</span>, image: Buffer): <span class="function"><span class="params">any</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> url = getHost(options.customUrl)</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> method: <span class="string">'PUT'</span>,</span><br><span class="line"> url: <span class="string">`<span class="subst">${url.protocol}</span>://<span class="subst">${options.zone}</span>.<span class="subst">${url.host}</span>/<span class="subst">${options.bucket}</span>/<span class="subst">${encodeURI(options.path)}</span>/<span class="subst">${encodeURI(fileName)}</span>`</span>,</span><br><span class="line"> headers: {</span><br><span class="line"> Host: <span class="string">`<span class="subst">${options.zone}</span>.<span class="subst">${url.host}</span>`</span>,</span><br><span class="line"> Authorization: signature,</span><br><span class="line"> <span class="built_in">Date</span>: <span class="keyword">new</span> <span class="built_in">Date</span>().toUTCString()</span><br><span class="line"> },</span><br><span class="line"> body: image,</span><br><span class="line"> resolveWithFullResponse: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="配置插件-Plugin-的-handle"><a href="#配置插件-Plugin-的-handle" class="headerlink" title="配置插件 Plugin 的 handle"></a>配置插件 Plugin 的 handle</h4><p>组合上述方法,处理上传逻辑</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> handle = <span class="keyword">async</span> (ctx: picgo): <span class="built_in">Promise</span><picgo> => {</span><br><span class="line"> <span class="keyword">const</span> qingstorOptions = ctx.getConfig(<span class="string">'picBed.qingstor-uploader'</span>)</span><br><span class="line"> <span class="keyword">if</span> (!qingstorOptions) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'Can\'t find the qingstor config'</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> imgList = ctx.output</span><br><span class="line"> <span class="keyword">const</span> customUrl = qingstorOptions.customUrl</span><br><span class="line"> <span class="keyword">const</span> path = qingstorOptions.path</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">in</span> imgList) {</span><br><span class="line"> <span class="keyword">const</span> signature = generateSignature(qingstorOptions, imgList[i].fileName)</span><br><span class="line"> <span class="keyword">let</span> image = imgList[i].buffer</span><br><span class="line"> <span class="keyword">if</span> (!image && imgList[i].base64Image) {</span><br><span class="line"> image = Buffer.from(imgList[i].base64Image, <span class="string">'base64'</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> options = postOptions(qingstorOptions, imgList[i].fileName, signature, image)</span><br><span class="line"> <span class="keyword">let</span> body = <span class="keyword">await</span> ctx.Request.request(options)</span><br><span class="line"> <span class="keyword">if</span> (body.statusCode === <span class="number">200</span> || body.statusCode === <span class="number">201</span>) {</span><br><span class="line"> <span class="keyword">delete</span> imgList[i].base64Image</span><br><span class="line"> <span class="keyword">delete</span> imgList[i].buffer</span><br><span class="line"> <span class="keyword">const</span> url = getHost(customUrl)</span><br><span class="line"> imgList[i][<span class="string">'imgUrl'</span>] = <span class="string">`<span class="subst">${url.protocol}</span>://<span class="subst">${qingstorOptions.zone}</span>.<span class="subst">${url.host}</span>/<span class="subst">${qingstorOptions.bucket}</span>/<span class="subst">${encodeURI(path)}</span>/<span class="subst">${imgList[i].fileName}</span>`</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'Upload failed'</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ctx</span><br><span class="line"> } <span class="keyword">catch</span> (err) {</span><br><span class="line"> <span class="keyword">if</span> (err.error === <span class="string">'Upload failed'</span>) {</span><br><span class="line"> ctx.emit(<span class="string">'notification'</span>, {</span><br><span class="line"> title: <span class="string">'上传失败!'</span>,</span><br><span class="line"> body: <span class="string">`请检查你的配置项是否正确`</span></span><br><span class="line"> })</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ctx.emit(<span class="string">'notification'</span>, {</span><br><span class="line"> title: <span class="string">'上传失败!'</span>,</span><br><span class="line"> body: <span class="string">'请检查你的配置项是否正确'</span></span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">throw</span> err</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="注册插件"><a href="#注册插件" class="headerlink" title="注册插件"></a>注册插件</h4><p>将 uploader 注册即可:</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> = <span class="function">(<span class="params">ctx: picgo</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> register = <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> ctx.helper.uploader.register(<span class="string">'qingstor-uploader'</span>, {</span><br><span class="line"> handle,</span><br><span class="line"> name: <span class="string">'青云 QingStor'</span>,</span><br><span class="line"> config: config</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> uploader: <span class="string">'qingstor-uploader'</span>,</span><br><span class="line"> register</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="发布插件"><a href="#发布插件" class="headerlink" title="发布插件"></a>发布插件</h3><ol><li><p>先登录 <a href="https://www.npmjs.com/" target="_blank" rel="noopener">npm</a> 账号</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm login</span><br></pre></td></tr></table></figure></li><li><p>发布到 npm 上就可以了</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm publish</span><br></pre></td></tr></table></figure></li></ol><!-- rebuild by neat -->]]></content>
<categories>
<category> Web-Front-End </category>
</categories>
<tags>
<tag> picgo-plugin-qingstor-uploader </tag>
<tag> 青云对象存储图床 </tag>
<tag> TypeScript </tag>
</tags>
</entry>
<entry>
<title>Android 自定义 View:包含多种状态的下载用圆形进度条</title>
<link href="/archives/CircleProgressBar.html"/>
<url>/archives/CircleProgressBar.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近做项目碰到一个这样的一个需求:需要一个环形的进度条表示一个下载请求的进度加载。<br>同时要以各种不同的图标展现其下载过程中的各个状态:等待、下载中、暂停、错误、完成。</p><p>具体状态对应图标见下图:<br><img src="https://img-cdn.pek3b.qingstor.com/CircleProgressBar/download_status.png" alt="download_status.png"></p><p>以上图标来自<a href="http://www.iconfont.cn/" target="_blank" rel="noopener">http://www.iconfont.cn/</a>。</p><p>考虑到其状态多达 5 种之多。用已有的控件组合显示,然后判断状态来控制各图标的显示不太合适。<br>借此机会,简单的撸一个这样的一个自定义控件:CircleProgressBar 来温习下自定义控件的知识。</p><p>直接拷贝 CircleProgressBar 使用:<a href="https://github.com/chengww5217/CircleProgressBarDemo/blob/master/app/src/main/java/com/chengww/circleprogressdemo/CircleProgressBar.java" target="_blank" rel="noopener">CircleProgressBar.java</a></p><a id="more"></a><h2 id="自定义控件"><a href="#自定义控件" class="headerlink" title="自定义控件"></a>自定义控件</h2><p>首先需要的基础知识,你需要了解关于安卓自定义控件的基本原理、控件的绘制过程。<br>推荐看下官方的相关文档 <a href="https://developer.android.com/guide/topics/ui/custom-components" target="_blank" rel="noopener">Custom View Components</a>。注意:文档为英文文档,有墙。</p><p>简单总结下见下表:<br><img src="https://img-cdn.pek3b.qingstor.com/CircleProgressBar/custom-components-form.png" alt="custom-components-form.png"></p><p>搞清楚上面的基础之后就正式开始自定义控件。如果还没有看过上述文档也可以跟着我把下面的步奏写一遍。</p><h3 id="创建-View"><a href="#创建-View" class="headerlink" title="创建 View"></a>创建 View</h3><p>一般自定义 View 都是继承自 android.view.View。不过既然我们自定义的是 ProgressBar,就没必要重头开始了,直接继承自 android.widget.ProgressBar 。<br>这样 setProgress(int progress); 这些基础方法就没必要再定义了。So,给我的控件取名为 <code>CircleProgressBar extends ProgressBar</code>。</p><p>观察上述几个图标,除了下载中状态有进度加载,其形态有所改变外,其余状态均为一个静态图片。现在只用搞定下载中状态的圆环进度和绘制中间的两条竖线即可。</p><h4 id="定义自定义属性"><a href="#定义自定义属性" class="headerlink" title="定义自定义属性"></a>定义自定义属性</h4><p>我们在使用 Android SDK 提供的控件的时候,可以直接从 <code>.xml</code> 文件中新建,比如新建一个 LinearLayout:</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">LinearLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:orientation</span>=<span class="string">"horizontal"</span> /></span></span><br></pre></td></tr></table></figure><p>同时我们还可以直接在 <code>.xml</code> 文件中配置各种属性,如上述代码中的 <code>android:orientation="horizontal"</code> 。<br>我们自定义的控件当然也要支持配置和一些自定义属性,所以就必须要这个构造方法:<code>public CircleProgressBar(Context context, AttributeSet attrs) {}</code>。<br>这个构造方法允许我们在 <code>.xml</code> 文件中创建和编辑我们自定义控件的实例:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">CircleProgressBar</span><span class="params">(Context context, AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>(context, attrs, <span class="number">0</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时,为了在 <code>.xml</code> 文件中定义我们的自定义属性(eg: color, size, etc.),我们需要新增以下构造方法:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">CircleProgressBar</span><span class="params">(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyleAttr)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs, defStyleAttr);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>defStyleAttr 这个整型变量是一个定义在 <code>res/values/attrs.xml</code> 文件中的 <code>declare-styleable</code> 值。<br>基于此,我们需要新建 <code>res/values/attrs.xml</code> 文件,并定义一些需要用到的自定义属性。</p><p>观察要实现的外圈进度条,有两个进度:一个用来表示默认的圆形,另一个表示进度的颜色。所以这里涉及到两个进度条颜色宽高的定义。要绘制圆肯定还需要半径。<br>故所有定义的属性如下:</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">resources</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">declare-styleable</span> <span class="attr">name</span>=<span class="string">"CircleProgressBar"</span>></span></span><br><span class="line"> <span class="comment"><!--默认圆的颜色--></span></span><br><span class="line"> <span class="tag"><<span class="name">attr</span> <span class="attr">name</span>=<span class="string">"defaultColor"</span> <span class="attr">format</span>=<span class="string">"color"</span> /></span></span><br><span class="line"> <span class="comment"><!--进度条的颜色--></span></span><br><span class="line"> <span class="tag"><<span class="name">attr</span> <span class="attr">name</span>=<span class="string">"reachedColor"</span> <span class="attr">format</span>=<span class="string">"color"</span> /></span></span><br><span class="line"> <span class="comment"><!--默认圆的高度--></span></span><br><span class="line"> <span class="tag"><<span class="name">attr</span> <span class="attr">name</span>=<span class="string">"defaultHeight"</span> <span class="attr">format</span>=<span class="string">"dimension"</span> /></span></span><br><span class="line"> <span class="comment"><!--进度条的高度--></span></span><br><span class="line"> <span class="tag"><<span class="name">attr</span> <span class="attr">name</span>=<span class="string">"reachedHeight"</span> <span class="attr">format</span>=<span class="string">"dimension"</span> /></span></span><br><span class="line"> <span class="comment"><!--圆的半径--></span></span><br><span class="line"> <span class="tag"><<span class="name">attr</span> <span class="attr">name</span>=<span class="string">"radius"</span> <span class="attr">format</span>=<span class="string">"dimension"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">declare-styleable</span>></span></span><br><span class="line"><span class="tag"></<span class="name">resources</span>></span></span><br></pre></td></tr></table></figure><p>这段代码声明了 5 个自定义属性,它们都是属于 styleable:CircleProgressBar 的。<br>为了方便起见,一般styleable的name和我们自定义控件的类名一样。自定义控件定义好了之后就可以直接使用了。<br>具体自定义属性值含义见 xml 里面的注释。</p><p>在使用中就可以直接设置这些自定义属性了:<br></p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">com.chengww.circleprogressdemo.CircleProgressBar</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"46dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"46dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:padding</span>=<span class="string">"6dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/cp_progress"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:defaultColor</span>=<span class="string">"#D8D8D8"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:reachedColor</span>=<span class="string">"#1296DB"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:defaultHeight</span>=<span class="string">"2.5dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:reachedHeight</span>=<span class="string">"2.5dp"</span> /></span></span><br></pre></td></tr></table></figure><p></p><h3 id="获取自定义属性"><a href="#获取自定义属性" class="headerlink" title="获取自定义属性"></a>获取自定义属性</h3><p>既然定义了自定义属性,当然需要获取到具体使用中设置的自定义属性。否则定义自定义属性就没有意义了。<br>首先定义成员变量:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> mDefaultColor;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> mReachedColor;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> mDefaultHeight;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> mReachedHeight;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> mRadius;</span><br><span class="line"><span class="keyword">private</span> Paint mPaint;</span><br><span class="line"><span class="keyword">private</span> Status mStatus = Status.Waiting;</span><br></pre></td></tr></table></figure><p>然后就是获取成员变量了。还记得我们上文中 Java 代码里面定义的构造方法 <code>public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {}</code> 吗?<br>没错,就是在这个方法里面获取用户设置的自定义属性值:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">CircleProgressBar</span><span class="params">(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyleAttr)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs, defStyleAttr);</span><br><span class="line"> TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);</span><br><span class="line"> <span class="comment">//默认圆的颜色</span></span><br><span class="line"> mDefaultColor = typedArray.getColor(R.styleable.CircleProgressBar_defaultColor, Color.parseColor(<span class="string">"#D8D8D8"</span>));</span><br><span class="line"> <span class="comment">//进度条的颜色</span></span><br><span class="line"> mReachedColor = typedArray.getColor(R.styleable.CircleProgressBar_reachedColor, Color.parseColor(<span class="string">"#1296DB"</span>));</span><br><span class="line"> <span class="comment">//默认圆的高度</span></span><br><span class="line"> mDefaultHeight = typedArray.getDimension(R.styleable.CircleProgressBar_defaultHeight, dp2px(context, <span class="number">2.5f</span>));</span><br><span class="line"> <span class="comment">//进度条的高度</span></span><br><span class="line"> mReachedHeight = typedArray.getDimension(R.styleable.CircleProgressBar_reachedHeight, dp2px(context, <span class="number">2.5f</span>));</span><br><span class="line"> <span class="comment">//圆的半径</span></span><br><span class="line"> mRadius = typedArray.getDimension(R.styleable.CircleProgressBar_radius, dp2px(context, <span class="number">17</span>));</span><br><span class="line"> typedArray.recycle();</span><br><span class="line"></span><br><span class="line"> setPaint();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当我们在 xml 文件中创建一个 View 时,所有在 xml 文件中声明的属性都会被传入到该 View 的上述构造方法中。<br>通过调用 Context 的 obtainStyledAttributes() 方法返回一个 TypedArray 对象。然后直接用 TypedArray 对象获取自定义属性的值,第二个参数是获取不到时取得默认值。<br>由于 TypedArray 对象是共享的资源,所以在获取完值之后必须要调用 recycle() 方法来回收。</p><h3 id="使用-Java-方法设置自定义属性"><a href="#使用-Java-方法设置自定义属性" class="headerlink" title="使用 Java 方法设置自定义属性"></a>使用 Java 方法设置自定义属性</h3><p>上述方法只能通过 xml 文件设置自定义属性,只有在 View 被初始化的时候才能获取到。要想在运行时使用 Java 方法修改某个属性值,对某个属性值(成员变量)新增 Getter 和 Setter 方法即可。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> Status mStatus = Status.Waiting;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> Status <span class="title">getStatus</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> mStatus;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setStatus</span><span class="params">(Status status)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (mStatus == status) <span class="keyword">return</span>;</span><br><span class="line"> mStatus = status;</span><br><span class="line"> invalidate();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意 setStatus 方法,在为 mStatus 赋值之后,调用了 invalidate() 方法,我们自定义控件的属性发生改变之后,控件的样子也可能发生改变,在这种情况下就需要调用 invalidate() 方法让系统去调用 View 的 onDraw() 重新绘制。<br>同样的,控件属性的改变可能导致控件所占的大小和形状发生改变,可以调用 requestLayout() 来请求测量获取一个新的布局位置。<br>注:如改变某属性后,确定控件不会变更大小和位置,可以不需要调用 requestLayout() 方法。同样,如控件不需要重绘,可以不需要调用 invalidate() 方法。</p><p>获取基础的一些属性,这里 mStatus 用来表示当前 View 的状态以对应各种下载状态。我们用这些状态来判定如何绘制合适的效果。各状态用一个内部枚举来表示。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> Status {</span><br><span class="line"> Waiting,</span><br><span class="line"> Pause,</span><br><span class="line"> Loading,</span><br><span class="line"> Error,</span><br><span class="line"> Finish</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述 setPaint() 为初始化 paint 方法。用以绘制进度圆环和各静态 Drawable。附上 setPaint() 方法代码:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setPaint</span><span class="params">()</span> </span>{</span><br><span class="line"> mPaint = <span class="keyword">new</span> Paint();</span><br><span class="line"> <span class="comment">//下面是设置画笔的一些属性</span></span><br><span class="line"> mPaint.setAntiAlias(<span class="keyword">true</span>);<span class="comment">//抗锯齿</span></span><br><span class="line"> mPaint.setDither(<span class="keyword">true</span>);<span class="comment">//防抖动,绘制出来的图要更加柔和清晰</span></span><br><span class="line"> mPaint.setStyle(Paint.Style.STROKE);<span class="comment">//设置填充样式</span></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Paint.Style.FILL :填充内部</span></span><br><span class="line"><span class="comment"> * Paint.Style.FILL_AND_STROKE :填充内部和描边</span></span><br><span class="line"><span class="comment"> * Paint.Style.STROKE :仅描边</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> mPaint.setStrokeCap(Paint.Cap.ROUND);<span class="comment">//设置画笔笔刷类型</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="处理-View-的布局"><a href="#处理-View-的布局" class="headerlink" title="处理 View 的布局"></a>处理 View 的布局</h3><h4 id="View-的测量"><a href="#View-的测量" class="headerlink" title="View 的测量"></a>View 的测量</h4><p>一个 View 在展示时总是其宽和高,测量 View 就是为了能够让自定义的控件能够根据各种不同的情况以合适的宽高去展示。<br>具体使用到的方法为 onMeasure() 方法。该方法重写自系统的方法,包含两个参数:int widthMeasureSpec, int heightMeasureSpec。<br>这两个参数包含了两个重要的信息:Mode 和 Size。获取 Mode 和 Size:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">int</span> specMode = MeasureSpec.getMode(measureSpec);</span><br><span class="line"><span class="keyword">int</span> specSize = MeasureSpec.getSize(measureSpec);</span><br></pre></td></tr></table></figure><p>以上代码可以获取 widthMode、heightMode、widthSize、heightSize 共四个参数。</p><p>Mode 代表了当前控件的父控件告诉我们控件,你应该按怎样的方式来布局。<br>Mode 有三个可选值:EXACTLY、AT_MOST、UNSPECIFIED。它们的含义是:</p><ul><li>EXACTLY:父控件告诉我们子控件了一个确定的大小,你就按这个大小来布局。比如我们指定了确定的 dp 值和 match_parent 的情况。</li><li>AT_MOST:当前控件不能超过一个固定的最大值,一般是 wrap_content 的情况。</li><li>UNSPECIFIED:当前控件没有限制,要多大就有多大,这种情况很少出现。</li></ul><p>Size 其实就是父布局传递过来的一个大小,父布局希望当前布局的大小。</p><p>下面是我们代码中 onMeasure() 方法的写法:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">onMeasure</span><span class="params">(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> widthMode = MeasureSpec.getMode(widthMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> heightMode = MeasureSpec.getMode(heightMeasureSpec);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> paintHeight = Math.max(mReachedHeight, mDefaultHeight);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (heightMode != MeasureSpec.EXACTLY) {</span><br><span class="line"> <span class="keyword">int</span> exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius * <span class="number">2</span> + paintHeight;</span><br><span class="line"> heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (widthMode != MeasureSpec.EXACTLY) {</span><br><span class="line"> <span class="keyword">int</span> exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius * <span class="number">2</span> + paintHeight;</span><br><span class="line"> widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">super</span>.onMeasure(widthMeasureSpec, heightMeasureSpec);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>我们只需要处理宽高没有精确指定的情况,通过 padding 加上整个圆以及 Paint 的宽度计算出具体的值。</p><p>接下来就是绘制效果了。</p><h3 id="绘制-View"><a href="#绘制-View" class="headerlink" title="绘制 View"></a>绘制 View</h3><p>如开始所述:观察上述几个图标,除了下载中状态有进度加载,其形态有所改变外,其余状态均为一个静态图片。绘制其余状态静态图片可以使用:<br><code>drawable.draw(canvas);</code> 方法。现在说说如何绘制下载中这个状态。</p><p>重写 onDraw() 方法,然后我们开始绘制圆:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">canvas.translate(getPaddingStart(), getPaddingTop());</span><br><span class="line">mPaint.setStyle(Paint.Style.STROKE);</span><br><span class="line"><span class="comment">//画默认圆(边框)的一些设置</span></span><br><span class="line">mPaint.setColor(mDefaultColor);</span><br><span class="line">mPaint.setStrokeWidth(mDefaultHeight);</span><br><span class="line">canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);</span><br></pre></td></tr></table></figure><p>通过 <code>canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);</code> 绘制默认状态下的圆。之后改变画笔的颜色,根据进度绘制圆弧。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//画进度条的一些设置</span></span><br><span class="line">mPaint.setColor(mReachedColor);</span><br><span class="line">mPaint.setStrokeWidth(mReachedHeight);</span><br><span class="line"><span class="comment">//根据进度绘制圆弧</span></span><br><span class="line"><span class="keyword">float</span> sweepAngle = getProgress() * <span class="number">1.0f</span> / getMax() * <span class="number">360</span>;</span><br><span class="line">canvas.drawArc(<span class="keyword">new</span> RectF(<span class="number">0</span>, <span class="number">0</span>, mRadius * <span class="number">2</span>, mRadius * <span class="number">2</span>), -<span class="number">90</span>, sweepAngle, <span class="keyword">false</span>, mPaint);</span><br></pre></td></tr></table></figure><p>最后绘制圆中间的两条竖线下载中状态就完成了。下面是一个示例,绘制竖线宽度为 2/5 半径(1/5 + 1/5),高度为 1/2 半径(1/2 + 1/2):</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">mPaint.setStyle(Paint.Style.STROKE);</span><br><span class="line">mPaint.setStrokeWidth(dp2px(getContext(), <span class="number">2</span>));</span><br><span class="line">mPaint.setColor(Color.parseColor(<span class="string">"#667380"</span>));</span><br><span class="line">canvas.drawLine(mRadius * <span class="number">4</span> / <span class="number">5</span>, mRadius * <span class="number">3</span> / <span class="number">4</span>, mRadius * <span class="number">4</span> / <span class="number">5</span>, <span class="number">2</span> * mRadius - (mRadius * <span class="number">3</span> / <span class="number">4</span>), mPaint);</span><br><span class="line">canvas.drawLine(<span class="number">2</span> * mRadius - (mRadius * <span class="number">4</span> / <span class="number">5</span>), mRadius * <span class="number">3</span> / <span class="number">4</span>, <span class="number">2</span> * mRadius - (mRadius * <span class="number">4</span> / <span class="number">5</span>), <span class="number">2</span> * mRadius - (mRadius * <span class="number">3</span> / <span class="number">4</span>), mPaint);</span><br></pre></td></tr></table></figure><p>然后通过判断 mStatus 来绘制不同的状态即可完成 onDraw() 方法即可。完整 onDraw() 代码和相关 dp2px 方法:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">onDraw</span><span class="params">(Canvas canvas)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onDraw(canvas);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 这里canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的</span></span><br><span class="line"><span class="comment"> * 当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,</span></span><br><span class="line"><span class="comment"> * 那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,</span></span><br><span class="line"><span class="comment"> * (比如:前面元素设置了平移或旋转的操作后,下一个元素在进行绘制之前执行了canvas.save();和canvas.restore()操作)这样后面的元素就不会受到(平移或旋转的)影响</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> canvas.save();</span><br><span class="line"> <span class="comment">//为了保证最外层的圆弧全部显示,我们通常会设置自定义view的padding属性,这样就有了内边距,所以画笔应该平移到内边距的位置,这样画笔才会刚好在最外层的圆弧上</span></span><br><span class="line"> <span class="comment">//画笔平移到指定paddingLeft, getPaddingTop()位置</span></span><br><span class="line"> canvas.translate(getPaddingStart(), getPaddingTop());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> mDiameter = (<span class="keyword">int</span>) (mRadius * <span class="number">2</span>);</span><br><span class="line"> <span class="keyword">if</span> (mStatus == Status.Loading) {</span><br><span class="line"> mPaint.setStyle(Paint.Style.STROKE);</span><br><span class="line"> <span class="comment">//画默认圆(边框)的一些设置</span></span><br><span class="line"> mPaint.setColor(mDefaultColor);</span><br><span class="line"> mPaint.setStrokeWidth(mDefaultHeight);</span><br><span class="line"> canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//画进度条的一些设置</span></span><br><span class="line"> mPaint.setColor(mReachedColor);</span><br><span class="line"> mPaint.setStrokeWidth(mReachedHeight);</span><br><span class="line"> <span class="comment">//根据进度绘制圆弧</span></span><br><span class="line"> <span class="keyword">float</span> sweepAngle = getProgress() * <span class="number">1.0f</span> / getMax() * <span class="number">360</span>;</span><br><span class="line"> canvas.drawArc(<span class="keyword">new</span> RectF(<span class="number">0</span>, <span class="number">0</span>, mRadius * <span class="number">2</span>, mRadius * <span class="number">2</span>), -<span class="number">90</span>, sweepAngle, <span class="keyword">false</span>, mPaint);</span><br><span class="line"></span><br><span class="line"> mPaint.setStyle(Paint.Style.STROKE);</span><br><span class="line"> mPaint.setStrokeWidth(dp2px(getContext(), <span class="number">2</span>));</span><br><span class="line"> mPaint.setColor(Color.parseColor(<span class="string">"#667380"</span>));</span><br><span class="line"> canvas.drawLine(mRadius * <span class="number">4</span> / <span class="number">5</span>, mRadius * <span class="number">3</span> / <span class="number">4</span>, mRadius * <span class="number">4</span> / <span class="number">5</span>, <span class="number">2</span> * mRadius - (mRadius * <span class="number">3</span> / <span class="number">4</span>), mPaint);</span><br><span class="line"> canvas.drawLine(<span class="number">2</span> * mRadius - (mRadius * <span class="number">4</span> / <span class="number">5</span>), mRadius * <span class="number">3</span> / <span class="number">4</span>, <span class="number">2</span> * mRadius - (mRadius * <span class="number">4</span> / <span class="number">5</span>), <span class="number">2</span> * mRadius - (mRadius * <span class="number">3</span> / <span class="number">4</span>), mPaint);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">int</span> drawableInt;</span><br><span class="line"> <span class="keyword">switch</span> (mStatus) {</span><br><span class="line"> <span class="keyword">case</span> Waiting:</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> drawableInt = R.mipmap.ic_waiting;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> Pause:</span><br><span class="line"> drawableInt = R.mipmap.ic_pause;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> Finish:</span><br><span class="line"> drawableInt = R.mipmap.ic_finish;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> Error:</span><br><span class="line"> drawableInt = R.mipmap.ic_error;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> Drawable drawable = getContext().getResources().getDrawable(drawableInt);</span><br><span class="line"> drawable.setBounds(<span class="number">0</span>, <span class="number">0</span>, mDiameter, mDiameter);</span><br><span class="line"> drawable.draw(canvas);</span><br><span class="line"> }</span><br><span class="line"> canvas.restore();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">float</span> <span class="title">dp2px</span><span class="params">(Context context, <span class="keyword">float</span> dp)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">float</span> scale = context.getResources().getDisplayMetrics().density;</span><br><span class="line"> <span class="keyword">return</span> dp * scale + <span class="number">0.5f</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="处理用户交互"><a href="#处理用户交互" class="headerlink" title="处理用户交互"></a>处理用户交互</h3><p>由于对于下载更新进度的情况来说,该控件只做状态显示,所以这一步不需要,要使用的话自己设置点击事件就可以了。</p><p>完成品效果 gif:</p><p><img src="https://img-cdn.pek3b.qingstor.com/CircleProgressBar/CircleProgressBarDemo.gif" alt="CircleProgressBarDemo.gif"></p><p>演示 apk 下载:<br><a href="https://js-cdn.pek3b.qingstor.com/files/CircleProgressBarDemo_1.0.apk" target="_blank" rel="noopener">https://blog.chengww.com/files/CircleProgressBarDemo_1.0.apk</a></p><p>源码下载:<a href="https://github.com/chengww5217/CircleProgressBarDemo" target="_blank" rel="noopener">https://github.com/chengww5217/CircleProgressBarDemo</a></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Android </category>
</categories>
<tags>
<tag> Android </tag>
<tag> 自定义 View </tag>
<tag> Custom View Components </tag>
<tag> CircleProgressBar </tag>
</tags>
</entry>
<entry>
<title>使用 Youtube-dl 下载 Youtube 1080P+ 视频</title>
<link href="/archives/youtube_download.html"/>
<url>/archives/youtube_download.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p><img src="https://img-cdn.pek3b.qingstor.com/youtube_download/youtube-crowd-uproar-protest.jpg" alt="youtube-crowd-uproar-protest.jpg"></p><p>鉴于 Youtube 将 1080P+ 画质的内容的音视频进行了分离。<br>之前的不少 Youtube 下载工具下载 1080P+ 画质都有一定程度的缺陷,现在 Youtube-dl 可以完美解决这个问题。</p><a id="more"></a><h3 id="What-is-it"><a href="#What-is-it" class="headerlink" title="What is it?"></a>What is it?</h3><p>来一段官网的介绍:</p><blockquote><p>youtube-dl is a command-line program to download videos from YouTube.com and a few <a href="http://rg3.github.io/youtube-dl/supportedsites.html" target="_blank" rel="noopener">more sites</a>. It requires the <a href="https://www.python.org/" target="_blank" rel="noopener">Python interpreter</a>, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work in your Unix box, in Windows or in Mac OS X. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. The project is currently being developed at GitHub.</p></blockquote><p>翻译下来大概的意思:</p><p>Youtube-dl 是一个命令行程序,用于从 YouTube.com 和<a href="http://rg3.github.io/youtube-dl/supportedsites.html" target="_blank" rel="noopener">其他视频网站</a>下载视频。其运行需要<a href="https://www.python.org/" target="_blank" rel="noopener">Python解释器</a>,要求版本 2.6, 2.7 或 3.2+,并且它是全平台兼容的软件。<br>它可以在您的 Unix,Windows 或 Mac OS X 机器中运行。该项目已开源,这意味着您可以对其进行修改,重新分发或随意使用它。该项目目前正在 GitHub 上开发。</p><p>该工具支持断点续传,下载某个视频中途关闭后,下次下载同一个视频,进度会恢复。<br>文件下载位置保存在命令行的当前目录,要想切换保存位置,请使用 cd 命令切换到其他目录。<br>如果您电脑上已经安装了 Ffmpeg,youtube-dl 还会自动合并音视频。</p><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>参考 <a href="https://www.runoob.com/python/python-install.html" target="_blank" rel="noopener">https://www.runoob.com/python/python-install.html</a> 安装 Python 3(Windows 系统记得勾选 <code>Add Python 3.x to Path</code> 以及安装 pip 选项)</p><p><img src="https://img-cdn.pek3b.qingstor.com/youtube_download/python_install_help_1.png" alt="python_install_help_1.png"></p><p><img src="https://img-cdn.pek3b.qingstor.com/youtube_download/python_install_help_2.png" alt="python_install_help_2.png"></p><p>前往 <a href="http://ffmpeg.org/download.html" target="_blank" rel="noopener">http://ffmpeg.org/download.html</a> 下载安装 FFmpeg ,并将解压后的 bin 目录设置为环境变量。</p><p>使用 pip 安装 youtube-dl<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">pip install youtube-dl</span><br></pre></td></tr></table></figure><p></p><h3 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h3><h4 id="设置代理"><a href="#设置代理" class="headerlink" title="设置代理"></a>设置代理</h4><p>鉴于国内的万里长城问题,这个要拿来作为第一位开讲了。<br>可以直接设置本次命令行的代理:</p><p>Windows: <code>set https_proxy=http://127.0.0.1:1080</code><br>Linux or Mac: <code>export https_proxy=http://127.0.0.1:1080</code></p><p>** 如不想使用全局代理,可以在每次需要代理的时候,<br>在每次 youtube-dl 命令后附上 <code>--proxy "https://127.0.0.1:1080"</code><br>如: <code>youtube-dl -F [url] --proxy "https://127.0.0.1:1080"</code></p><h4 id="列举视频所有格式"><a href="#列举视频所有格式" class="headerlink" title="列举视频所有格式"></a>列举视频所有格式</h4><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">youtube-dl -F [url]</span><br></pre></td></tr></table></figure><p>或者<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">youtube-dl --list-formats [url]</span><br></pre></td></tr></table></figure><p></p><p>这是一个列清单参数,执行后并不会下载视频,但能知道这个目标视频都有哪些格式存在,下一步进行下载的格式选择。</p><p>举例:输入 <code>youtube-dl -F https://www.youtube.com/watch?v=UyJ8Qbh_LH0</code><br>将出现以下回应<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[youtube] UyJ8Qbh_LH0: Downloading webpage</span><br><span class="line">[youtube] UyJ8Qbh_LH0: Downloading video info webpage</span><br><span class="line">[youtube] UyJ8Qbh_LH0: Downloading js player vfl-mZlA8</span><br><span class="line">[info] Available formats for UyJ8Qbh_LH0:</span><br><span class="line">format code extension resolution note</span><br><span class="line">249 webm audio only DASH audio 55k , opus @ 50k, 1.64MiB</span><br><span class="line">250 webm audio only DASH audio 70k , opus @ 70k, 2.10MiB</span><br><span class="line">171 webm audio only DASH audio 115k , vorbis@128k, 3.88MiB</span><br><span class="line">140 m4a audio only DASH audio 128k , m4a_dash container, mp4a.40.2@128k, 4.47MiB</span><br><span class="line">251 webm audio only DASH audio 131k , opus @160k, 3.96MiB</span><br><span class="line">278 webm 256x144 144p 96k , webm container, vp9, 30fps, video only, 3.28MiB</span><br><span class="line">160 mp4 256x144 144p 110k , avc1.4d400c, 30fps, video only, 3.12MiB</span><br><span class="line">242 webm 426x240 240p 224k , vp9, 30fps, video only, 7.39MiB</span><br><span class="line">133 mp4 426x240 240p 318k , avc1.4d4015, 30fps, video only, 7.36MiB</span><br><span class="line">243 webm 640x360 360p 413k , vp9, 30fps, video only, 14.03MiB</span><br><span class="line">134 mp4 640x360 360p 712k , avc1.4d401e, 30fps, video only, 15.74MiB</span><br><span class="line">244 webm 854x480 480p 759k , vp9, 30fps, video only, 25.41MiB</span><br><span class="line">135 mp4 854x480 480p 1075k , avc1.4d401f, 30fps, video only, 24.75MiB</span><br><span class="line">136 mp4 1280x720 720p 1477k , avc1.4d401f, 30fps, video only, 35.71MiB</span><br><span class="line">247 webm 1280x720 720p 1513k , vp9, 30fps, video only, 45.11MiB</span><br><span class="line">17 3gp 176x144 small , mp4v.20.3, mp4a.40.2@ 24k, 2.68MiB</span><br><span class="line">36 3gp 320x180 small , mp4v.20.3, mp4a.40.2, 7.84MiB</span><br><span class="line">18 mp4 640x360 medium , avc1.42001E, mp4a.40.2@ 96k, 23.76MiB (best)</span><br></pre></td></tr></table></figure><p></p><h3 id="选取格式下载"><a href="#选取格式下载" class="headerlink" title="选取格式下载"></a>选取格式下载</h3><p>可以选择单视频,单音频,也可以选择视频 + 音频 合并。<br>下载指定质量的视频和音频并自动合并:<br><code>youtube-dl -f [format code] [url]</code> (请注意该 f 为小写)<br>通过上一步获取到了所有视频格式的清单,最左边一列就是编号对应着不同的格式.<br>由于 YouTube 的 1080p 及以上的分辨率都是音视频分离的,所以我们需要分别下载视频和音频,可以使用 <code>247+251</code> (或 <code>bestvideo+bestaudio</code>)这样的组合.<br>如果系统中安装了 FFmpeg 的话, youtube-dl 会自动合并下下好的视频和音频, 然后自动删除单独的音视频文件。</p><p>举例:<br>输入 <code>youtube-dl -f 247+251 https://www.youtube.com/watch?v=UyJ8Qbh_LH0</code><br>或输入 <code>youtube-dl -f bestvideo+bestaudio https://www.youtube.com/watch?v=UyJ8Qbh_LH0</code></p><p>将出现以下回应<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">[youtube] UyJ8Qbh_LH0: Downloading webpage</span><br><span class="line">[youtube] UyJ8Qbh_LH0: Downloading video info webpage</span><br><span class="line">[youtube] UyJ8Qbh_LH0: Downloading js player vfls4aurX</span><br><span class="line">[download] Destination: 小さな恋のうた/mongol800(Cover)-UyJ8Qbh_LH0.f247.webm</span><br><span class="line">[download] 1.7% of 45.11MiB at 171.76KiB/s ETA 04:24</span><br></pre></td></tr></table></figure><p></p><h3 id="其他命令"><a href="#其他命令" class="headerlink" title="其他命令"></a>其他命令</h3><p>下载字幕</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">youtubd-dl --write-sub [url] //这样会下载一个vtt格式的英文字幕和mkv格式的1080p视频下来</span><br><span class="line"></span><br><span class="line">youtube-dl --write-sub --skip-download [url] //下载单独的vtt字幕文件,而不会下载视频</span><br><span class="line"></span><br><span class="line">youtube-dl --write-sub --all-subs [url] //下载所有语言的字幕(如果有的话)</span><br><span class="line"></span><br><span class="line">youtube-dl --write-auto-sub [url] //下载自动生成的字幕(YouTube only)</span><br></pre></td></tr></table></figure><p>更多命令参见项目 github 首页: <a href="https://github.com/rg3/youtube-dl" target="_blank" rel="noopener">https://github.com/rg3/youtube-dl</a></p><h3 id="命令集合"><a href="#命令集合" class="headerlink" title="命令集合"></a>命令集合</h3><p>只是简单下载视频的话,一直输入命令未免太过繁琐。附上可以免输入的批处理脚本和 shell 脚本。<br>(注:下载位置,代理等请自行核对修改)</p><p>windows 机器复制下方批处理脚本,以文本方式保存,修改后缀为 <code>.bat</code> 保存。</p><figure class="highlight bat"><table><tr><td class="code"><pre><span class="line">@<span class="built_in">echo</span> off</span><br><span class="line">:<span class="built_in">start</span></span><br><span class="line">::设置下载位置</span><br><span class="line"><span class="built_in">set</span> <span class="built_in">dir</span>=C:</span><br><span class="line"><span class="built_in">pushd</span> <span class="variable">%dir%</span></span><br><span class="line"><span class="keyword">if</span> /i <span class="keyword">not</span> <span class="variable">%dir%</span>==<span class="variable">%cd%</span> <span class="keyword">goto</span> :<span class="built_in">start</span></span><br><span class="line"><span class="built_in">echo</span> 油管下载器,有代理。当前保存路径:<span class="variable">%cd%</span></span><br><span class="line"><span class="built_in">echo</span> 设置代理 http_proxy=http://<span class="number">127</span>.<span class="number">0</span>.<span class="number">0</span>.<span class="number">1</span>:<span class="number">1080</span></span><br><span class="line"><span class="built_in">set</span> http_proxy=http://<span class="number">127</span>.<span class="number">0</span>.<span class="number">0</span>.<span class="number">1</span>:<span class="number">1080</span></span><br><span class="line">:download</span><br><span class="line"><span class="built_in">set</span> /p input=请输入视频链接:</span><br><span class="line"><span class="built_in">set</span> input=<span class="variable">%input:&=^^^&%</span></span><br><span class="line">youtube-dl -F <span class="variable">%input%</span></span><br><span class="line"><span class="keyword">if</span> <span class="keyword">errorlevel</span> <span class="number">1</span> <span class="keyword">goto</span> :download</span><br><span class="line"><span class="built_in">set</span> /p code=请输入视频格式编号:</span><br><span class="line">youtube-dl -f <span class="variable">%code%</span> <span class="variable">%input%</span></span><br><span class="line">::--external-downloader aria2c --external-downloader-args "-x <span class="number">8</span> -k <span class="number">1</span>M"</span><br><span class="line"><span class="keyword">goto</span> :download</span><br></pre></td></tr></table></figure><p>Linux or Mac 复制下方 shell 脚本,以文本方式保存,修改后缀为 <code>.sh</code> 保存。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment">#设置下载路径</span></span><br><span class="line"><span class="built_in">export</span> dir=~</span><br><span class="line"><span class="function"><span class="title">start</span></span>() {</span><br><span class="line"><span class="keyword">if</span> [[ $(<span class="built_in">cd</span> <span class="variable">$dir</span>;<span class="built_in">pwd</span>) != <span class="variable">$dir</span> ]]</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line"><span class="built_in">echo</span> 不能设置下载目录为<span class="variable">$dir</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line">}</span><br><span class="line"><span class="built_in">echo</span> 油管下载器,有代理。当前保存路径:<span class="variable">$dir</span></span><br><span class="line">start</span><br><span class="line"><span class="built_in">echo</span> 设置代理 http_proxy=http://127.0.0.1:1080</span><br><span class="line"><span class="built_in">export</span> http_proxy=http://127.0.0.1:1080</span><br><span class="line"><span class="function"><span class="title">download</span></span>(){</span><br><span class="line"><span class="built_in">echo</span> 请输入视频格式编号:</span><br><span class="line"><span class="built_in">read</span> code</span><br><span class="line">youtube-dl -f <span class="variable">$code</span> <span class="variable">$input</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">while</span> [[ <span class="literal">true</span> ]]; <span class="keyword">do</span></span><br><span class="line"><span class="built_in">echo</span> 请输入视频链接:</span><br><span class="line"> <span class="built_in">read</span> input</span><br><span class="line"> youtube-dl -F <span class="variable">$input</span></span><br><span class="line"> <span class="keyword">if</span> [[ $? -eq 0 ]]</span><br><span class="line"> <span class="keyword">then</span></span><br><span class="line"> download</span><br><span class="line"> <span class="keyword">fi</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><!-- rebuild by neat -->]]></content>
<categories>
<category> tools </category>
</categories>
<tags>
<tag> youtube-dl </tag>
<tag> Youtube </tag>
<tag> YouTube 下载 </tag>
</tags>
</entry>
<entry>
<title>Java 兼容 Let’s Encrypt 证书</title>
<link href="/archives/Java_compatible_certificate_of_Lets_Encrypt.html"/>
<url>/archives/Java_compatible_certificate_of_Lets_Encrypt.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>最近公司打算将网站 HTTPS 证书更换为 Let’s Encrypt 的证书。虽然现在主流浏览器已经信任 Let’s Encrypt 证书了,但是对于一些 Java 老版本,还是会出现不兼容的情况。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_compatible_certificate_of_Lets_Encrypt/hello_world_lets_encrypt.png" alt="hello_world_lets_encrypt.png"></p><p>为解决此问题,本文应运而生。</p><a id="more"></a><h2 id="什么是-Let’s-Encrypt"><a href="#什么是-Let’s-Encrypt" class="headerlink" title="什么是 Let’s Encrypt"></a>什么是 Let’s Encrypt</h2><p>Let’s Encrypt 是一个免费,自动化和开放的证书颁发机构(CA),为公益而运行,由 <a href="https://letsencrypt.org/isrg/" target="_blank" rel="noopener">Internet Security Research Group(ISRG)</a> 提供服务。</p><p>Let’s Encrypt 由 Mozilla、Cisco、Akamai、IdenTrust、EFF 等组织人员发起,主要的目的是为了推进网站从 HTTP 向 HTTPS 过度的进程,目前已经有越来越多的商家加入和赞助支持,其证书现在已经可以被所有主流的浏览器所信任。</p><h2 id="证书兼容性"><a href="#证书兼容性" class="headerlink" title="证书兼容性"></a>证书兼容性</h2><p>因 Let’s Encrypt 证书较新,下列 JDK/JRE 旧版本会不信任 Let’s Encrypt 证书<br>(查看 Java 版本:命令行输入 <em>java -version</em> ):</p><ul><li>Java 7 < 7u111</li><li>Java 8 < 8u101</li></ul><p>而抛出以下异常:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException</span><br><span class="line">[... 以下输出省略 ...]</span><br></pre></td></tr></table></figure><p>详细兼容性问题参考:<a href="https://letsencrypt.org/docs/certificate-compatibility/" target="_blank" rel="noopener">https://letsencrypt.org/docs/certificate-compatibility/</a></p><h2 id="检查-Java-环境是否兼容-Let’s-Encrypt-证书"><a href="#检查-Java-环境是否兼容-Let’s-Encrypt-证书" class="headerlink" title="检查 Java 环境是否兼容 Let’s Encrypt 证书"></a>检查 Java 环境是否兼容 Let’s Encrypt 证书</h2><h3 id="自行编写程序测试"><a href="#自行编写程序测试" class="headerlink" title="自行编写程序测试"></a>自行编写程序测试</h3><p>自行编写测试程序查看 Java 环境是否支持 Let’s Encrypt 证书:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">new</span> URL(<span class="string">"https://helloworld.letsencrypt.org"</span>).openConnection().connect();</span><br></pre></td></tr></table></figure><p>然后查看是否抛出以下异常即可:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException</span><br><span class="line">[... 以下输出省略 ...]</span><br></pre></td></tr></table></figure><h3 id="使用-SSLPing-进行测试"><a href="#使用-SSLPing-进行测试" class="headerlink" title="使用 SSLPing 进行测试"></a>使用 SSLPing 进行测试</h3><p>如还没有测试程序,可以使用 ping 测试程序:SSLPing(可测试任何 SSL/TLS 端口,不仅是 HTTPS)。下面将使用预先编译的 SSLPing.jar 进行测试(阅读源码后自行编译也非常容易):</p><p>在命令行输入以下内容以克隆 SSLPing 这个项目(请确保已安装 git)或点击链接下载 <a href="https://github.com/dimalinux/SSLPing/raw/master/dist/SSLPing.jar" target="_blank" rel="noopener">SSLPing.jar</a></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git clone https://github.com/dimalinux/SSLPing.git</span><br></pre></td></tr></table></figure><p>成功后进行测试:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443</span><br></pre></td></tr></table></figure><p>然后查看是否抛出以下异常即可:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">About to connect to <span class="string">'helloworld.letsencrypt.org'</span> on port <span class="number">443</span></span><br><span class="line">javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException</span><br><span class="line">[... 以下输出省略 ...]</span><br></pre></td></tr></table></figure><h2 id="问题解决"><a href="#问题解决" class="headerlink" title="问题解决"></a>问题解决</h2><p>出现这种证书不兼容的情况,只是因为 Let’s Encrypt 证书太新,Java 老版本未将其加入根证书导致。<br>具体解决办法有两种共三个:</p><ul><li>1.更新 Java update 版本<br>比如 JDK8_8u100,升级到 JDK8_8u101 及以上就可以了。但是搞 Java 的都是老学究,怕出现兼容问题,坚决不肯升级,所以不推荐。</li><li>2.自行将 Let’s Encrypt 证书加入信任<br>Chrome 浏览器打开一个使用 Let’s Encrypt 证书的网站:<a href="https://chengww.com">https://chengww.com</a>,可以看到一共有三个证书:<br><img src="https://img-cdn.pek3b.qingstor.com/Java_compatible_certificate_of_Lets_Encrypt/letsencrypt_certificate.png" alt="letsencrypt_certificate.png"></li></ul><p>现在关键是将哪个证书加入信任的问题。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_compatible_certificate_of_Lets_Encrypt/isrg-keys.png" alt="isrg-keys.png"></p><p>参考 Let’s Encrypt 官网 <a href="https://letsencrypt.org/certificates/" target="_blank" rel="noopener">Chain of Trust</a> 里面的这段说明:</p><blockquote><p>Our roots are kept safely offline. We issue end-entity certificates to subscribers from the intermediates in the next section.</p></blockquote><p>根证书 <a href="https://letsencrypt.org/certs/isrgrootx1.pem.txt" target="_blank" rel="noopener">ISRG Root X1 (self-signed)</a> 是离线安全保存的,在下一节中向中间人发放终端实体证书。</p><blockquote><p>IdenTrust has cross-signed our intermediates. This allows our end certificates to be accepted by all major browsers while we propagate our own root.</p></blockquote><blockquote><p>Under normal circumstances, certificates issued by Let’s Encrypt will come from “Let’s Encrypt Authority X3”. The other intermediate, “Let’s Encrypt Authority X4”, is reserved for disaster recovery and will only be used should we lose the ability to issue with “Let’s Encrypt Authority X3”. The X1 and X2 intermediates were our first generation of intermediates. We’ve replaced them with new intermediates that are more compatible with Windows XP.</p></blockquote><p>IdenTrust 和 Let’s Encrypt 中间证书已经交叉签名,故所有主流浏览器都接受 Let’s Encrypt 的结束证书。</p><p>正常情况下,Let’s Encrypt 颁发的证书将来自“Let’s Encrypt Authority X3”。另一个中间件“Let’s Encrypt Authority X4”保留用于灾难恢复,只有在Let’s Encrypt 失去发出“Let’s Encrypt Authority X3”的能力时才会使用。X1和X2中间体是Let’s Encrypt 的第一代中间体。Let’s Encrypt 用与Windows XP更兼容的新中间体替换它们。</p><p>也就是现在只有 <a href="https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt" target="_blank" rel="noopener">Let’s Encrypt Authority X3</a> 是正在签名用的。将其加入信任即可。</p><p>参考解决方法(任选其一):</p><h3 id="往-JRE-中导入-Let’s-Encrypt-证书(无需修改代码,推荐)"><a href="#往-JRE-中导入-Let’s-Encrypt-证书(无需修改代码,推荐)" class="headerlink" title="往 JRE 中导入 Let’s Encrypt 证书(无需修改代码,推荐)"></a>往 JRE 中导入 Let’s Encrypt 证书(无需修改代码,推荐)</h3><p>可以将 Let’s Encrypt 证书加入 JRE 的信任证书,这种方式无需修改代码,简单快捷,推荐使用。</p><h4 id="操作系统为-Mac-OS-X-或-Linux"><a href="#操作系统为-Mac-OS-X-或-Linux" class="headerlink" title="操作系统为 Mac OS X 或 Linux"></a>操作系统为 Mac OS X 或 Linux</h4><ul><li>检查 <strong>JAVA_HOME</strong> 已经正确配置</li></ul><p>在终端上输入以下内容:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">echo $JAVA_HOME</span><br></pre></td></tr></table></figure><p>出现以下类似回应即为正确配置:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/</span><br></pre></td></tr></table></figure><p>如未配置请自行搜索配置 Java 环境变量即可。</p><ul><li>下载 Let’s Encrypt 中间证书</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem</span><br></pre></td></tr></table></figure><p>或直接复制上述链接下载即可</p><ul><li>导入证书</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">keytool -trustcacerts -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file "lets-encrypt-x3-cross-signed.pem"</span><br></pre></td></tr></table></figure><p>出现</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Certificate was added to keystore</span><br></pre></td></tr></table></figure><p>即可</p><p><strong>(注:当出现 java.io.FileNotFoundException… 时可能要检查相关文件路径是否正确)</strong></p><h4 id="操作系统为-Windows"><a href="#操作系统为-Windows" class="headerlink" title="操作系统为 Windows"></a>操作系统为 Windows</h4><ul><li>检查 <strong>JAVA_HOME</strong> 已经正确配置</li></ul><p>在命令行上输入以下内容:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">echo %JAVA_HOME%</span><br></pre></td></tr></table></figure><p>出现以下类似回应即为正确配置:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">C:\Program Files (x86)\Java\jdk1.8.0_92</span><br></pre></td></tr></table></figure><p>如未配置请自行搜索配置 Java 环境变量即可。</p><ul><li>下载 Let’s Encrypt 中间证书</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem</span><br></pre></td></tr></table></figure><p>或直接复制上述链接下载即可</p><ul><li>导入证书</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cd %JAVA_HOME%\bin</span><br><span class="line">keytool -trustcacerts -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file "lets-encrypt-x3-cross-signed.pem"</span><br></pre></td></tr></table></figure><p>出现</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Certificate was added to keystore</span><br></pre></td></tr></table></figure><p>即可。</p><p><strong>(注:当出现 java.io.FileNotFoundException… 时可能要检查相关文件路径是否正确)</strong></p><p>该方法至此已经完成,请检测是否成功。</p><hr><h3 id="程序运行时添加-Let’s-Encrypt-证书为信任证书"><a href="#程序运行时添加-Let’s-Encrypt-证书为信任证书" class="headerlink" title="程序运行时添加 Let’s Encrypt 证书为信任证书"></a>程序运行时添加 Let’s Encrypt 证书为信任证书</h3><p>也可以在程序初始化或网络初始化时将 Let’s Encrypt 证书添加进信任证书。</p><p>使用火狐浏览器访问 <a href="https://helloworld.letsencrypt.org" target="_blank" rel="noopener">https://helloworld.letsencrypt.org</a> ,然后将 <strong>Let’s Encrypt Authority X3</strong> 导出为 <strong>.cer</strong> 文件,或点击下载 <a href="https://lets-encrypt.pek3a.qingstor.com/Let's%20Encrypt%20Authority%20X3.cer" target="_blank" rel="noopener">Let’s Encrypt Authority X3.cer</a></p><p>将文件地址替换下述文件地址中: <strong>“Let’s Encrypt Authority X3.cer”</strong></p><p>具体请参考以下示例添加:</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Created by chengww on 2018/9/18.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.net.URL;</span><br><span class="line"><span class="keyword">import</span> java.net.URLConnection;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.net.ssl.SSLHandshakeException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SSLExample</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">initTrustManager</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// Enter the path of the file named 'Let's Encrypt Authority X3.cer'</span></span><br><span class="line"> System.setProperty(<span class="string">"javax.net.ssl.trustStore"</span>, <span class="string">"Let's Encrypt Authority X3.cer"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> initTrustManager();</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// signed by default trusted CAs.</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://www.thawte.com"</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// signed by letsencrypt</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://helloworld.letsencrypt.org"</span>));</span><br><span class="line"> <span class="comment">// signed by LE's cross-sign CA</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://letsencrypt.org"</span>));</span><br><span class="line"> <span class="comment">// qingstorage</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://stor.qingstorage.com"</span>));</span><br><span class="line"> <span class="comment">// qingcloud</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://www.qingcloud.com/"</span>));</span><br><span class="line"> <span class="comment">// self-signed</span></span><br><span class="line"> testUrl(<span class="keyword">new</span> URL(<span class="string">"https://www.pcwebshop.co.uk/"</span>));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">testUrl</span><span class="params">(URL url)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> URLConnection connection = url.openConnection();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> connection.connect();</span><br><span class="line"> System.out.println(<span class="string">"Headers of "</span> + url + <span class="string">" => "</span></span><br><span class="line"> + connection.getHeaderFields());</span><br><span class="line"> } <span class="keyword">catch</span> (SSLHandshakeException e) {</span><br><span class="line"> System.out.println(<span class="string">"Untrusted: "</span> + url);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该方法至此已经完成,请检测是否成功。</p><hr><h3 id="升级-JDK-JRE-版本"><a href="#升级-JDK-JRE-版本" class="headerlink" title="升级 JDK/JRE 版本"></a>升级 JDK/JRE 版本</h3><p>可以直接升级的 JDK/JRE update 版本,7u111 及 8u101 之后已将 Let’s Encrypt 证书加入信任。</p><p>前往 <a href="https://www.oracle.com/technetwork/java/javase/downloads/index.html" target="_blank" rel="noopener">https://www.oracle.com/technetwork/java/javase/downloads/index.html</a> 下拉到最后一项 Java Archive,点击 <strong>DOWNLOAD</strong></p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_compatible_certificate_of_Lets_Encrypt/Java_Archive.png" alt="Java_Archive.png"></p><p>选择 Accept License Agreement,下载对应的 JDK/JRE 版本后安装即可</p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_compatible_certificate_of_Lets_Encrypt/Accept_License_Agreement.png" alt="Accept_License_Agreement.png"></p><p>该方法至此已经完成,请检测是否成功。</p><hr><h2 id="检查是否成功"><a href="#检查是否成功" class="headerlink" title="检查是否成功"></a>检查是否成功</h2><p>重复操作上述 <strong>检查 Java 环境是否兼容 Let’s Encrypt 证书</strong> 的内容即可。</p><hr><!-- rebuild by neat -->]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Let’s Encrypt </tag>
</tags>
</entry>
<entry>
<title>VCD--国产专利之痛</title>
<link href="/archives/VCD-Chinese_patent_pain.html"/>
<url>/archives/VCD-Chinese_patent_pain.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>世界上第一台家用VCD机,影音光碟(Video Compact Disc;VCD)诞生于中国合肥的”万燕”之手,”万燕”让中国的老百姓认识了VCD,并开创了中国的VCD行业。</p><p>VCD在二十世纪末消费类电子领域里,是中国可能领先的唯一机会,而在此之前本领域没有一个中国人创造并形成产业。但是当时万燕集团的掌门人、也是VCD机研制者姜万勐先生犯下令他后悔终生的致命错误:他竟然认为在当时的情况,申不申请专利没有太大的意义,以致万燕推出的第一批1000台VCD机,几乎都被国内外家电公司买去做了样机,成为解剖的对象。</p><p>随后,索尼、松下、飞利浦等国外制造业巨大纷纷加强对VCD机的研究,推出新的专利技术,刷新VCD的技术标准,以致包括万燕集团在内的国内家电企业生产VCD需要向这些国外巨头缴纳巨额的专利费用,在之后的七八年时间里,这些专利费用在国内市场就累积上百亿之多。</p><p>仅仅3到4年时间,拥有这项领先技术的万燕却最终淹没于同行业的激烈竞争中。而VCD产业,随着进入者的增多,产品利润被不断摊薄,最后这项没什么技术含量的产业,也在低价竞争中日薄西山,被DVD蚕食殆尽。</p><a id="more"></a><h3 id="VCD的前世今生"><a href="#VCD的前世今生" class="headerlink" title="VCD的前世今生"></a>VCD的前世今生</h3><h4 id="起源"><a href="#起源" class="headerlink" title="起源"></a>起源</h4><p>世界上第一台VCD视盘机是由国人姜万勐先生发明的,诞生于安徽万燕公司。<br>事情发生在1992年。在美国举办的国际广播电视技术展览会上。美国C-CUBE(斯高柏)公司曾展出了一项图像解压缩技术。时任安徽观代集团总经理的姜万勐先生受其启发, 敏锐地意识到:可以把图像和声音存储在一张比较小的光盘里的MPEG技术,意味着可以创造出一种物美价廉的视听产品,供老百姓在家中使用。于是在1993年,出资57万元,研制出了物美价廉的VCD(价格相比同期录像机很有优势)。同年12月,他又与美籍华人孙燕生(时任C-CUBE公司董事长)共同投资1700万美元成立了万燕公司,各取了姜万勐、孙燕生名字中的一个字作为公司名称:<strong>安徽省万燕电子系统有限公司</strong>。</p><p>但是令人匪夷所思的是,VCD真正的核心技术:万燕公司花费巨资委托C-CUBE研发的解码芯片技术,却牢牢掌握在C-CUBE斯高柏微系统公司手里。这为后来国产VCD乱局埋下了崩溃的种子。</p><h4 id="万燕的危机"><a href="#万燕的危机" class="headerlink" title="万燕的危机"></a>万燕的危机</h4><p>在1993年安徽现代电视技术研究所的VCD可行性报告中,有这样的一段描述:</p><blockquote><p>这是本世纪末消费类电子领域里,中国可能领先的惟一机会。</p></blockquote><p>为此,姜万勐进行了一系列的市场调查,得到了一系列的数字:1993年中国市场上组合音响的销售量是142万台,录像机的销售量是170余万台,LD影碟机100万台,CD激光唱机是160余万台。当时的LD光盘是四五百元一张,而VCD机的光盘价格却只有它的10%左右,因此可以预测,VCD机每年的销售量将会达到200万台左右。</p><h5 id="第一批VCD"><a href="#第一批VCD" class="headerlink" title="第一批VCD"></a>第一批VCD</h5><p>中国的老百姓到了1994年底才逐渐认识VCD。在这一年,万燕生产了几万台VCD机。不仅如此,姜万勐还要开发碟片,总不能让老百姓买了枪而没子弹。为此,他又向11家音像出版社购买了版权,推出了97种卡拉OK碟片。在最初成立不到一年的时间里,“万燕”倾其所有,开创了一个市场,确立了一个响当当的品牌,并形成了一整套成型的技术,独霸于VCD天下。<br>可以说,万燕的初创是成功的,也是辉煌的。但是,万燕也给自己酿下一杯苦酒。令姜万勐感到伤心的是,万燕推出的第一批1000台VCD机,几乎都被国内外各家电公司买去做了样机,成为解剖的对象。<br>1994年,万燕开始批量生产VCD,但初期由于片源不配套,使VCD在市场发展上停滞了很长的一段时间。</p><p>万燕所面临的难题是软硬件要一齐开发。“万燕”在前期研究开发的投入是1600万美元,广告投入是2000万元人民币,中国百姓到了1994年底才逐渐认识VCD,而在这一年,“万燕”生产了几万台VCD,结果只卖出了2万台。由于前期投入太多,导致早期产品成本高达每台360美元,再加广告费用,在市场上每台VCD卖四五千元,却基本无利可赚。 不仅如此,万燕还要开发碟片,万燕为此又向11家音像出版社购买版权,推出97种卡拉OK碟片。</p><p>投入上亿地研发资金,产品研发出来,却没有及时申请专利,进行技术垄断。其他制造商只需要花费极小的代价就能获得成套的成熟生产技术。<br>在前期的产品成本上,万燕公司就吃了大亏;接下来的败招是九四年就投入2000万的广告费用。</p><p>一直到九五年。影碟机市场还属于培育阶段,在盗版影碟大量涌现之前,影碟机的市场容量很有限。投入再多地广告费都不会起到预期地效果。<br>广告是需要连续进行投入的商业行为,一旦中断,广告就会随着时间地延长而变得毫无效果。<br>要是将2000万广告预算拖到95下半年再投入,或许能让万燕公司起死回生。<br>万燕前期投入上亿的研究资金、2000万的广告费用,迄今为止卖出去地台影碟机还没超过两万台,就算每台影碟机的售价高达四千元,万燕收回成本都困难,更不要说什么利润了。<br>万燕资金裢脱节了!<br>万燕公司在影碟机市场启动的前夜就花光了所有钱,陷入生存的危机之中。</p><p>万燕要打翻身仗,唯有继续筹集资金扩大产能,等到九五、九六年影碟机市场突然暴发的时候,利用先机大举抢占市场。<br>万燕会成功的筹集到资金吗?显然不会,不然万燕就不会是被遗忘的品牌了。<br>在盗版影碟大量出现之前,影碟机的惨淡市场、万燕的惨淡经营已经让投资人失出信心。最重要的一点就是引入影碟机整套的生产技术甚至不需要一百万的资金,然而投资万燕却要分摊万燕公司前期投入研发的上亿元成本,哪个投资人会傻到做这折本的买卖?</p><h5 id="市场蜂拥,万燕沉没"><a href="#市场蜂拥,万燕沉没" class="headerlink" title="市场蜂拥,万燕沉没"></a>市场蜂拥,万燕沉没</h5><p>1996年开始到1997年,中国的VCD市场每年以数倍的速度增长。从1995年的60万台猛增至1996年600多万台,1997年销售达到1000万台。只用了短短5年,VCD影碟机累计销售已有5000万台,并催生了爱多、步步高、新科等国内响当当的品牌。但 “万燕”却在这个产业中,从“先驱”成为“先烈”,其市场份额从100%跌到2%,也就在这一年,“万燕”被同省的美菱集团重组,成为美菱万燕公司。<br>“万燕”让中国百姓认识了VCD,但摘桃子的却是深谙市场秘诀的广东人。 此时,深谙市场秘诀的广东人却恰到好处地把握了这个良机。由于VCD整机组装对技术要求不高,没有生产许可证的限制,再加上市场已经被打开,广东又是散件水货的聚集地,几个因素凑到一起,VCD组装厂如雨后春笋般出现在了珠江三角洲。“床板工厂”开始遍布大街小巷,一个人一天可以组装10台、20台,一家老少一天就能装出几十台,市场颇为火爆。<br>在中国家电产品中,没有一种产品如VCD 般以狂飙突进的方式席卷全国。“万燕”从市场上衰落后,VCD进入了爆炸式的增长时期,有关统计显示,中国VCD企业最多的时候达1000多家,整个VCD行业风云变幻,其铺天盖地的广告攻势,高开低走的价格走势,大起大落的市场命运,无不让人刻骨铭心。</p><p>待续…</p><p>中国VCD发展历程</p><ul><li>1993年9月,留美学者姜万勐、孙燕生生产出世界上第一台VCD。</li><li>1996年至1997年,爱多、新科等新品牌开始大规模进入市场,并占据VCD大部分市场。</li><li>1998年9月,发生了全国性的SVCD与CVD标准之争论。1998年8月,信息产业部制定《超级VCD系统行业规范》,于1998年11月1日生效。</li><li>1998年10月至1999年7月,各大影碟机厂家不断推出附加新技术的VCD产品,如可播放MP3和MIDI的超级VCD,掌上型超级VCD和可录写的超级VCD。甚至实现了VCD联网和语音复读等功能,以实现中小学的VCD辅助教学。</li><li>1999年1月,影碟机行业广告费投入直线下降。在激烈竞争中,不少知名企业陷入困境,如“小霸王”倒闭和“爱多”亏损严重。</li><li>1999年7月,各主要生产厂家不约而同地开始大规模降价,普通单碟机的价格纷纷跌破800元/台。DVD产品开始取代VCD。</li></ul><p>本文综合以下文章整理:</p><ul><li>万燕VCD的衰败之路(20110629)<br><a href="https://wenku.baidu.com/view/d203a14469eae009581bec16.html" target="_blank" rel="noopener">https://wenku.baidu.com/view/d203a14469eae009581bec16.html</a></li><li>万燕VCD的悲剧<br><a href="http://www.360doc.com/content/16/0512/15/14771698_558528759.shtml" target="_blank" rel="noopener">http://www.360doc.com/content/16/0512/15/14771698_558528759.shtml</a></li><li>中国VCD行业案例分析<br><a href="https://wenku.baidu.com/view/69283f040740be1e650e9a2a.html" target="_blank" rel="noopener">https://wenku.baidu.com/view/69283f040740be1e650e9a2a.html</a></li></ul><!-- rebuild by neat -->]]></content>
<categories>
<category> 科技随笔 </category>
</categories>
<tags>
<tag> VCD </tag>
</tags>
</entry>
<entry>
<title>使用JAVA合并哔哩哔哩手机客户端下载的视频</title>
<link href="/archives/Java_merge_videos_of_bilibili.html"/>
<url>/archives/Java_merge_videos_of_bilibili.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p>使用哔哩哔哩手机客户端下载的视频在电脑上播放,无奈视频是分段的,每次都只好手动的合并再播放。而且客户端下载的视频不会按网页文件名命名,而是以av号–全数字命名。最可怕的是,每次打开一集的时候,进入的目录层级得吓死人。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_merge_videos_of_bilibili/bilibili-video-level.png" alt="视频层级"></p><p>最最可怕的是,新版客户端默认文件后缀是 <em>.blv</em> 。难道我们要一个一个重命名然后再合并吗?<br>NO!这种重复的事情交给计算机就好了。</p><p>自己动手丰衣足食,我们就动手写个JAVA版的哔哩哔哩视频合并小程序。</p><p>完整项目地址: <a href="http://git.oschina.net/chengww5217/BiliBiliMerge" target="_blank" rel="noopener">http://git.oschina.net/chengww5217/BiliBiliMerge</a><br>直接下载使用:<br><a href="http://git.oschina.net/chengww5217/BiliBiliMerge/raw/master/run/BilibiliMeroV1.2.7z" target="_blank" rel="noopener">http://git.oschina.net/chengww5217/BiliBiliMerge/raw/master/run/BilibiliMeroV1.2.7z</a><br>使用帮助:<br><a href="http://git.oschina.net/chengww5217/BiliBiliMerge/blob/master/README.md" target="_blank" rel="noopener">http://git.oschina.net/chengww5217/BiliBiliMerge/blob/master/README.md</a></p><a id="more"></a><h4 id="实现功能"><a href="#实现功能" class="headerlink" title="实现功能"></a>实现功能</h4><ul><li>1.自动识别文件夹下视频文件并进行合并</li><li>2.合并后以视频播放页视频名称+视频分 P 名称命名<br><em>F:(日剧)夺爱之冬\第一话.flv</em></li><li>3.合并完成删除源文件</li></ul><h4 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h4><ul><li>1.得到哔哩哔哩客户端下载的视频目录</li></ul><p>将哔哩哔哩手机客户端下载的视频移出手机的 Android 目录,如移动到根目录<br>因 Android MTP 限制,电脑无法访问 Android 目录。此目录是 Android 应用缓存目录。<br>视频位于 <em>Android–data–tv.danmaku.bili(最下面)–download</em> 下。如图显示的数字目录即为需求目录。请将数字目录移出Android目录外。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Java_merge_videos_of_bilibili/bilibili-video-level-mobile.png" alt=""></p><p>手机连上电脑后,将上述数字目录复制或移动到电脑。</p><ul><li>2.分析视频目录结构</li></ul><p><em>8896746\1\entry.json</em> 这个json包含了整个播放目录的名称和每一P的名称<br><em>8896746\1\lua.flv.bili2api.3.blv</em> 这个文件夹就是各分段视频文件了。<br>注意:视频文件命名逻辑是:<em>0.blv,1.blv…9.blv,10.blv…</em><br>也就是说,一旦视频文件超过 10 个,如 0-10,合并的时候会出现这样的合并顺序:<em>0.blv–1.blv–10.blv–2.blv…</em> 所以说,我们需要先把 <em>0.blv-9.blv</em> 重命名为 <em>00.blv-09.blv</em></p><ul><li>3.FLV科普</li></ul><blockquote><p>FLV是一个二进制文件,由文件头(FLV header)和很多tag组成。tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流(关键字或者文件信息之类)。<br>FLV文件=FLV头文件+ tag1+tag内容1 + tag2+tag内容2 + …+… + tagN+tag内容N。</p></blockquote><p>也就是说合并FLV分段视频的时候不能简单粗暴的将多个flv视频片段按字节流的方式写到一个文件中。<br>这时候来看FLV合并的原理:</p><blockquote><p>(1) flv 文件由1个header和若干个tag组成;<br>(2) header记录了视频的元数据;<br>(3) tag 是有时间戳的数据;<br>(4) flv合并的原理就是把多个文件里的tag组装起来,调整各tag的时间戳。<br>(5)判断是否为第一个文件,是则安装头部。</p></blockquote><ul><li>了解了这些就可以动手撰写我们的合并程序了。Let’s go.</li></ul><h4 id="流程逻辑"><a href="#流程逻辑" class="headerlink" title="流程逻辑"></a>流程逻辑</h4><ul><li><p>提示输入哔哩哔哩下载的视频文件夹(输入文件夹),输入输出的文件夹。<br>因最后合并完成后要删除源文件,故要求输出文件夹不能和输入文件夹相同。<br>一次输入多个输入文件夹以英文逗号隔开。</p></li><li><p>然后进入输入文件夹下– <em>entry.json</em> 得到视频名称,和输入文件夹拼接创建目录。<br>如:输出到 <em>F:\视频名称</em> 文件夹</p></li><li><p>执行合并<br>listFiles()执行两次进入到这个文件夹</p></li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Java_merge_videos_of_bilibili/bilibili-video-list-files.png" alt=""></p><p>entry.json 得到视频每一P的名称,拼接输出如 <em>F:\视频名称\第一话.flv</em><br>判断进入 <em>lua.flv.bili2api.3</em> 文件夹即可得到所有视频文件<br>判断对 <em>0.flv-9.flv</em> 进行重命名—> <em>00.flv-09.flv</em><br>进行合并操作</p><ul><li>删除源文件</li></ul><h4 id="程序"><a href="#程序" class="headerlink" title="程序"></a>程序</h4><ul><li>1.首先eclipse建项目<br>包结构很简单</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Java_merge_videos_of_bilibili/bilibili-merge-package.png" alt="包结构"></p><ul><li>2.输入输出文件夹<br>包含main方法的Bilibili.java<br>输入输出文件夹</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"> File out;</span><br><span class="line">File[] in = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">while</span>(<span class="keyword">true</span>){</span><br><span class="line"><span class="keyword">boolean</span> isBreak = <span class="keyword">true</span>;</span><br><span class="line">Scanner scanner = <span class="keyword">new</span> Scanner(System.in);</span><br><span class="line">String line = scanner.nextLine();</span><br><span class="line"><span class="keyword">if</span>(line == <span class="keyword">null</span> || line.length() == <span class="number">0</span>){</span><br><span class="line">System.out.println(<span class="string">"输入不为空,请重试:"</span>);</span><br><span class="line">isBreak = <span class="keyword">false</span>;</span><br><span class="line">}<span class="keyword">else</span>{</span><br><span class="line">String[] lines = line.split(<span class="string">","</span>);</span><br><span class="line">in = <span class="keyword">new</span> File[lines.length];</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < lines.length;i++){</span><br><span class="line">in[i] = <span class="keyword">new</span> File(lines[i]);</span><br><span class="line"><span class="keyword">if</span>(!in[i].exists()){</span><br><span class="line">System.out.println(in[i].getAbsolutePath() + <span class="string">"文件夹不存在,请重试:"</span>);</span><br><span class="line">isBreak = <span class="keyword">false</span>;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span>(isBreak){</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">System.out.println(<span class="string">"请输入输出路径:"</span>);</span><br><span class="line"><span class="keyword">while</span>(<span class="keyword">true</span>){</span><br><span class="line">Scanner scanner = <span class="keyword">new</span> Scanner(System.in);</span><br><span class="line">String line = scanner.nextLine();</span><br><span class="line">out = <span class="keyword">new</span> File(line);</span><br><span class="line"><span class="keyword">if</span>(!out.exists()){</span><br><span class="line">System.out.println(<span class="string">"文件夹不存在,请重试:"</span>);</span><br><span class="line">}<span class="keyword">else</span>{</span><br><span class="line"><span class="keyword">boolean</span> isEquals = <span class="keyword">true</span>;</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < in.length;i++){</span><br><span class="line"><span class="keyword">if</span>(out.getAbsolutePath().equals(in[i].getAbsolutePath())){</span><br><span class="line">isEquals = <span class="keyword">false</span>;</span><br><span class="line">System.out.println(<span class="string">"输出路径和某个输入路径相同,请重试:"</span>);</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span>(isEquals){</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>3.循环读取多个输入目录的视频名称</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//循环</span></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < in.length;i++){</span><br><span class="line"><span class="comment">//得到播放文件名,如"(日剧)夺爱之冬"</span></span><br><span class="line">String path = in[i].getAbsolutePath() +separator+ <span class="string">"1"</span>+separator+<span class="string">"entry.json"</span>;</span><br><span class="line">String line = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">BufferedReader reader = </span><br><span class="line"> <span class="keyword">new</span> BufferedReader(<span class="keyword">new</span> InputStreamReader(<span class="keyword">new</span> FileInputStream(path), Charset.forName(<span class="string">"utf-8"</span>))); </span><br><span class="line">line = reader.readLine();</span><br><span class="line">reader.close();</span><br><span class="line">System.out.println(<span class="string">"json="</span>+line);</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//输出路径</span></span><br><span class="line">String[] names = tool.json_getName(line);</span><br><span class="line">String episode_path = out.getAbsolutePath() + separator + names[<span class="number">0</span>];</span><br><span class="line">File episode = <span class="keyword">new</span> File(episode_path);</span><br><span class="line"><span class="keyword">if</span>(!episode.exists()){</span><br><span class="line">episode.mkdirs();</span><br><span class="line">}</span><br><span class="line">System.out.println(<span class="string">"输出:"</span>+episode_path);</span><br><span class="line"><span class="comment">//合并</span></span><br><span class="line">tool.doMerge(in[i], episode_path);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>4.判断对 <em>0.flv-9.flv</em> 进行重命名—> <em>00.flv-09.flv</em> 后合并</li></ul><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public void doMerge(File in,String episode_path){</span><br><span class="line">//1、2、3、4...</span><br><span class="line">File[] files = in.listFiles();</span><br><span class="line"></span><br><span class="line">//循环</span><br><span class="line">for(File f : files){</span><br><span class="line">//文件名,如第一话</span><br><span class="line">String name = null;</span><br><span class="line">//获得所有名为.blv的文件</span><br><span class="line">File[] ffs = null;</span><br><span class="line">File[] fs = f.listFiles();</span><br><span class="line">for(final File ff : fs){</span><br><span class="line">if(ff.getName().equals("entry.json")){</span><br><span class="line">String json_name = null;</span><br><span class="line">try {</span><br><span class="line">BufferedReader reader = </span><br><span class="line"> new BufferedReader(new InputStreamReader(new FileInputStream(ff), Charset.forName("utf-8")));</span><br><span class="line">json_name = reader.readLine();</span><br><span class="line">reader.close();</span><br><span class="line">} catch (Exception e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line">name = json_getName(json_name)[1];</span><br><span class="line">}</span><br><span class="line">if(ff.isDirectory() && ff.getName().startsWith("lua.")){</span><br><span class="line">//重命名</span><br><span class="line">for(int i = 0; i < ff.list().length;i++){</span><br><span class="line">File pathname = ff.listFiles()[i];</span><br><span class="line">//0.blv -- 00.blv</span><br><span class="line">if(pathname.getName().endsWith(".blv") && pathname.getName().length() == 5){</span><br><span class="line">pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".blv"));</span><br><span class="line">}</span><br><span class="line">if(pathname.getName().endsWith(".flv") && pathname.getName().length() == 5){</span><br><span class="line">pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".flv"));</span><br><span class="line">}</span><br><span class="line">//0.blv.bdl -- 00.blv.bdl</span><br><span class="line">if(pathname.getName().endsWith(".blv.bdl") && pathname.getName().length() == 9){</span><br><span class="line">pathname.renameTo(new File(pathname.getParentFile().getAbsolutePath() + File.separator + "0" + i + ".blv.bdl"));</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">ffs = ff.listFiles(new FileFilter() {</span><br><span class="line"></span><br><span class="line">public boolean accept(File pathname) {</span><br><span class="line">for(int i = 0;i < ff.list().length;i++){</span><br><span class="line">if(pathname.getName().endsWith(".blv") || pathname.getName().endsWith(".flv") || pathname.getName().endsWith(".blv.bdl")){</span><br><span class="line">return true;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">return false;</span><br><span class="line">}</span><br><span class="line">});</span><br><span class="line">//合并</span><br><span class="line">System.out.println("开始合并...");</span><br><span class="line">FlvMerge mFlvMerge = new FlvMerge();</span><br><span class="line">try {</span><br><span class="line">mFlvMerge.merge(ffs, new File(episode_path + File.separator + name + ".flv"));</span><br><span class="line">} catch (IOException e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>5.递归删除操作</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">deleteFolder</span><span class="params">(File file)</span></span>{ </span><br><span class="line"> <span class="keyword">if</span>(!file.exists()){ </span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>; </span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">if</span>(file.isFile() || file.listFiles().length == <span class="number">0</span>){ </span><br><span class="line"> file.delete(); </span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>; </span><br><span class="line"> }<span class="keyword">else</span>{ </span><br><span class="line"> File[] files = file.listFiles(); </span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>;i<files.length;i++){ </span><br><span class="line"> deleteFolder(files[i]); </span><br><span class="line"> } </span><br><span class="line"> file.delete(); </span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>6.具体怎么对FLV视频进行合并的,<a href="http://git.oschina.net/chengww5217/BiliBiliMerge/blob/master/src/com/chengww/tools/FlvMerge.java" target="_blank" rel="noopener">请点击这里</a> ,注释比较清晰。</li></ul><!-- rebuild by neat -->]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Java </tag>
<tag> Bilibili </tag>
<tag> FlvMerge </tag>
</tags>
</entry>
<entry>
<title>写给产品经理之前端是如何展示后端数据的</title>
<link href="/archives/How_does_the_front_end_display_the_back-end_data.html"/>
<url>/archives/How_does_the_front_end_display_the_back-end_data.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>移动互联网的迅猛发展让移动APP呈现出爆发之势,这两年更是移动开发程序员的春天。</p><p><img src="https://img-cdn.pek3b.qingstor.com/How_does_the_front_end_display_the_back-end_data/get_your_shit_together.png" alt=""></p><p>今天的互联网上充斥着产品与技术的撕逼。也许你会问产品经理到底要不要懂技术?由此引申出,产品经理到底要不要懂设计?产品经理到底要不要懂运营?产品经理到底要不要懂市场?产品经理到底要不要懂业务?这所有问题的来源都是大家对于产品经理的工作认识不一致。</p><a id="more"></a><p><strong>每个人心中都有一个产品经理的定义,产品经理在技术方面更多的是去统筹和规划。不是画画图写写文档就可以了。这里面更多的需要的是对逻辑的梳理和拆分。</strong><br>例如很简单的一个app内嵌发红包的活动,产品经理需要确定整个活动的流程。从用户进入页面的那一瞬间就应该被产品经理掌控。他的每一个步骤,每一个操作会带来什么结果,有哪些变量会导致活动进程失败,这些都要产品去考虑。等到活动逻辑和过程全部梳理完成,下面就要进行拆分了。还是以发红包为例,红包中金额是客户端写死还是服务端进行计算,红包领取资格需要服务端返回几种结果,每种结果对应客户端的提示是什么,用户领取红包以后服务端需要记录那些信息(帐号,uid,领取时间,金额,是否使用等),客户端哪些地方需要加入计数器进行数据统计。总结起来其实就是,产品经理需要根据开发的每一个环节,将所有内容分类整理,并分发给不同部分的开发进行研发。最后,还需要给测试准备好check list,当测试按照check list测试完成以后,才可以上线。</p><p>以上种种都需要产品对前端如何显示后端数据有一个清晰的认识才能规划好数据如何展示。是APP写死呢还是后台返回,在用户任务进行的时候有哪些可能case。只有搞清楚这些,产品才能在实际的开发中掌握好整个项目的流程与进展,才能不被开发给糊弄。</p><h3 id="1-前后端到底在干些什么"><a href="#1-前后端到底在干些什么" class="headerlink" title="1.前后端到底在干些什么"></a>1.前后端到底在干些什么</h3><p>简单的说,前端仅仅将后端返回的数据展示在页面上,不做过多的逻辑处理。前端需要关心的是,数据如何更好的展示出来,页面效果如何做出来,以及其性能问题。<br>而后端就是负责对这些数据进行处理,提供给前端展示。</p><p>前端一般有H5、android、ios等多端界面。数据不要轻易写死在前端里面,不然到时候三端都要修改,费时费力。而将这些变化多数据让后端进行处理,前端将这个数据取出来显示出来就行了。</p><p>举个例子吧,下图是一个美团app首页团购的展示效果<br><img src="https://img-cdn.pek3b.qingstor.com/How_does_the_front_end_display_the_back-end_data/meituan.jpeg" alt="美团"></p><p>上方美食等8个icon一般为固定展示栏目,非特殊情况不会修改。那么前端一般是写死在app中,等到需要更新的时候更新app即可。</p><p>而下方猜你喜欢是一个列表,该列表数据经常变化,数据写在服务端维护,app取出数据进行展示即可。</p><h3 id="2-前端到底是怎么显示数据的"><a href="#2-前端到底是怎么显示数据的" class="headerlink" title="2.前端到底是怎么显示数据的"></a>2.前端到底是怎么显示数据的</h3><p>首先,前段对页面的展示是分两步走的。<br>第一、在本地绘制好界面,当然此时未连api会填充一些假数据,或写一些默认值。<br>第二、连api进行数据获取,将数据填充进界面。</p><p>既然如此,咱们简单看下前端拿到的数据到底长什么样的吧。<br>现在前端获取到的数据基本是json数据。</p><ul><li>何谓json数据<br>JSON是一种传递对象的语法,对象可以是name/value对,数组和其他对象。<br>拿刚才美团截图里面的猜你喜欢列表简单说下吧,如下面这一坨东西就是后端可能返回给前端的数据<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> "list": [</span><br><span class="line"> {</span><br><span class="line"> "title": "合辑护甲",</span><br><span class="line"> "content": "【北京市】奥斯卡货到付款..."</span><br><span class="line"> "price": 12.9</span><br><span class="line"> "distance": "145.0km"</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> "title": "合辑护甲",</span><br><span class="line"> "content": "【北京市】奥斯卡货到付款..."</span><br><span class="line"> "price": 12.9</span><br><span class="line"> "distance": "145.0km"</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></li></ul><p>不需要特别懂里面每一个的含义,只需要知道,前端通过”title”这个键名(key)就可以拿到”合辑护甲”这个值(value)。<code>"title": "合辑护甲"</code> 这一整个就是俗称的一个字段。通过该字段前端就可以获取到列表的标题了。当然这个列表只是简单的展示用,还有诸如图片地址、优惠信息、已售份额等信息没有提供,这就是缺少字段的情况。<br>前后端就是通过这样的一个定义获取/传递数据的。</p><h3 id="3-什么样的数据该由前端来控制,什么样的数据该由后端提供呢"><a href="#3-什么样的数据该由前端来控制,什么样的数据该由后端提供呢" class="headerlink" title="3.什么样的数据该由前端来控制,什么样的数据该由后端提供呢"></a>3.什么样的数据该由前端来控制,什么样的数据该由后端提供呢</h3><p>考虑到后期拓展、需求变更等,一般来说,涉及到计算的、可能有变动的,一律不要让前端来弄。<br>还是刚才那个例子,在刚才那个列表中有一个“立减5元”的橙黄色tag。<br>这个tag信息,如果考虑不充分,比如说,后端只提供一个数字5,然后前端在其页面写死“立减x元”,x为填入后端提供的数字,颜色固定为橙黄色。这个如果需求不修改还好,如果后期需要新增一个“满20减5元”的红色tag不傻眼了吗?<br>到时候只能通过升级app来解决,而且不升级的老用户将永远看不到这个红色的tag。<br>所以说,考虑到程序的可复用和拓展性,需要产品将后期可能新增或变更的需求考虑好,和前后端进行沟通,让前后端设计好实现,尽量降低程序的耦合和硬编码。这才能使一个产品更加健壮以及让苦逼的程序猿少加班的关键。</p><p>那么刚才那个tag的需求如何设计才合理呢?<br>首先是tag显示文字,全权由后端提供,可以是多个字段,由前端进行拼接。然后是tag的颜色,提供几种样式让前端判断是一种可行的办法,但是直接提供tag的色值给前端的这种方式无疑给前端展示增加了无限的可能。<br>是不是也要增加一个tag形状的字段呢?<br>俗话说,过犹不及。tag形状这种东西真的很少变更,字段太多无疑会增加开发的时间成本,而且会让人有一种舍本逐末之感。</p><h3 id="4-前端数据刷新时机问题"><a href="#4-前端数据刷新时机问题" class="headerlink" title="4.前端数据刷新时机问题"></a>4.前端数据刷新时机问题</h3><p>前端获取到后端数据后,如果前端不主动刷新重新请求数据的话,这个页面的数据在该页面销毁前会一直保持不变。</p><p>如何理解?<br>首先,第一次进入一个页面,该页面数据为空或默认数据。此时前端会链接api请求数据。数据请求完成后,填充进页面。那么本次联网请求就完成了,在前端不进行二次数据请求之前,该页面会一直保持本次请求的数据。也就是说,就算服务端修改了数据,前端此时是不能事实的进行更新的,因为我前端不知道你数据更新了。</p><p>那么在这个需要实时更新页面数据的时候和前端讲这种需求会被前端直接回绝:“做不了”。这个时候产品咋办,怪怪妥协?最后背锅的还是自己,而且自己也不知道是真做不了还是假做不了。</p><p>实时刷新也不是不能做,只是做的成本略高,需要和后端进行配合,像微信聊天一样和后端进行长连接(socket),这样服务端数据变更前端就知道数据变更了。<br>当然如果稍懂页面刷新机制的话,可以要求前端在适当的时机进行页面刷新,如在页面可见的时候进行刷新,这样用户每次看到的都是最新的数据。也可以让用户主动刷新,如新增刷新功能。</p><h3 id="5-One-more-thing"><a href="#5-One-more-thing" class="headerlink" title="5.One more thing"></a>5.One more thing</h3><p>产品懂技术这件事情,不仅会降低和开发同学沟通时的难度和被歧视风险,还会提升在面对开发同学意见时的判断力,会降低被技术团队忽悠的几率。同时,在需要向上级解释技术原因导致变更的情况下,也会有助于说服老板。<br>“闻道有先后,术业有专攻”,要相信自己所接触的开发团队是专业的,靠谱的,相信开发团队为实现需求所做出的技术方案是合理的,最优的。如果有质疑,可以加深沟通,以合适的方式提出自己的质疑。这里要补充一句的是,这个信任过程是需要建立的,也是会根据团队的表现不断变化的;同理,其实团队对于产品经理的信任度也是一样的情况。<br>吐槽是没有意义的,关键还是要解决问题。如果觉得产品经理不靠谱,那么有没有进行过深入的沟通?如果觉得开发不好沟通,那么有没有进行过了解,不好沟通的原因在哪里?如果当事人本身确实不可沟通,那么有没有考虑和对方的老板沟通,或者通过自己的老板如实反映情况?吐槽是最容易的,解决问题反而会很难。</p><!-- rebuild by neat -->]]></content>
<categories>
<category> 给产品经理讲技术 </category>
</categories>
<tags>
<tag> 给产品经理讲技术 </tag>
</tags>
</entry>
<entry>
<title>安卓指纹+密码支付(解锁)仿支付宝Demo</title>
<link href="/archives/Persional_Android_fingerprint_useage.html"/>
<url>/archives/Persional_Android_fingerprint_useage.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h2 id="1-前言"><a href="#1-前言" class="headerlink" title="1.前言"></a>1.前言</h2><p>Google 从 Android6.0(api23)就开始提供标准指纹识别支持,并对外提供指纹识别相关的接口。但是Android上的指纹识别似乎就是用来解锁手机屏幕,三方APP应用指纹的也是寥寥无几。一直想踩下安卓指纹识别的坑,直到这两天终于空出时间来尝试下android指纹识别的应用。</p><p>好吧,废话少说,show me the code,先上 Demo 截图:</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/finger-demo-shot.gif" alt=""></p><a id="more"></a><h2 id="2-使用指纹识别"><a href="#2-使用指纹识别" class="headerlink" title="2.使用指纹识别"></a>2.使用指纹识别</h2><p>点击指纹识别 button,弹出如图弹窗,弹窗使用 DialogFragment。具体实现请看下面</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/finger-demo-finger-shot.png" alt=""></p><h3 id="官方标准库"><a href="#官方标准库" class="headerlink" title="官方标准库"></a>官方标准库</h3><p>Google 提供的与指纹识别相关的核心类不多,主类是 FingerprintManager,主类依赖三个内部类,如下图所示:</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/google-finger-class01.png" alt=""></p><p>FingerprintManager 主要提供三个方法如下:</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/google-finger-class02.png" alt=""></p><p>FingerprintManager.AuthenticationCallback 类提供的回调接口如下,重点区分红色下划线标注的部分</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/google-finger-class03.png" alt=""></p><p><strong>启动指纹识别接口</strong></p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/google-finger-class04.png" alt=""></p><p>看了上面的介绍,如果要写代码就变得简单了</p><p><strong>1. AndroidManifest 权限声明</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><uses-permission android:name="android.permission.USE_FINGERPRINT"/></span><br></pre></td></tr></table></figure><p><strong>2. 获取 FingerManager 服务对象</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">public static FingerprintManager getFingerprintManager(Context context) { </span><br><span class="line"> FingerprintManager fingerprintManager = null;</span><br><span class="line"> try {</span><br><span class="line"> fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE); </span><br><span class="line"> } catch (Throwable e) { </span><br><span class="line"> Log.e("TAG","have not class FingerprintManager");</span><br><span class="line"> } </span><br><span class="line"> return fingerprintManager;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>3. 启动指纹识别</strong></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">mFingerprintManager.authenticate(cryptoObject, mCancellationSignal, 0, mAuthCallback, null);</span><br></pre></td></tr></table></figure><p><strong>官方v4兼容包</strong></p><p>上面介绍最标准的官方实现指纹识别的方式,当然适配肯定没这么简单,因为有很多设备兼容性要考虑,<br>Google 后续在 v4 包中提供了一套完整的实现,实现类与上面的一一对应的,<br>就是改了个名字(FingerprintManager 改为了 FingerprintManagerCompat,<br>机智的发现 Compat 是兼容的意思,所以 Google 在 v4 包中做了一些兼容性处理),<br>做了很多兼容处理,官方推荐使用后者。v4 包中类结构如下:<br><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/google-finger-class-v4.png" alt=""><br>v4包中的类使用与上面标准库中的一致,就是名字不一样而已,这里不再介绍使用方式。</p><h2 id="3-使用密码解锁"><a href="#3-使用密码解锁" class="headerlink" title="3.使用密码解锁"></a>3.使用密码解锁</h2><p>指纹识别失败达到一定次数调用密码解锁,同指纹识别弹窗一样使用 DialogFragment。<br>用这个 DialogFragment 有个坑,稍后再讲。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Persional_Android_fingerprint_useage/finger-demo-dialog-fragment.png" alt=""></p><p>密码解锁弹窗样式,fragment_pwd.xml</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">RelativeLayout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">LinearLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginLeft</span>=<span class="string">"40dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginRight</span>=<span class="string">"40dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginTop</span>=<span class="string">"100dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:background</span>=<span class="string">"@drawable/shape_dialog"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:orientation</span>=<span class="string">"vertical"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:paddingBottom</span>=<span class="string">"@dimen/spacing_large"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">RelativeLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">style</span>=<span class="string">"@style/style_black_normal_text"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"@dimen/text_item_height"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_centerInParent</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:gravity</span>=<span class="string">"center"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"请输入密码"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">ImageView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/iv_close"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"20dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"20dp"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:background</span>=<span class="string">"@drawable/selector_item_pressed"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_alignParentRight</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_centerVertical</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginRight</span>=<span class="string">"@dimen/spacing_tiny"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:src</span>=<span class="string">"@mipmap/icon_del"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"></<span class="name">RelativeLayout</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">View</span> <span class="attr">style</span>=<span class="string">"@style/style_separate_line"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">com.chengww.fingerdemo.PwdView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/pwdView"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginLeft</span>=<span class="string">"@dimen/spacing_large"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginRight</span>=<span class="string">"@dimen/spacing_large"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:background</span>=<span class="string">"@color/white"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/tv_miss_pwd"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">style</span>=<span class="string">"@style/style_blue_normal_text"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginTop</span>=<span class="string">"@dimen/text_item_right_margin"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginEnd</span>=<span class="string">"@dimen/text_item_right_margin"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_marginRight</span>=<span class="string">"@dimen/text_item_right_margin"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"忘记密码?"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:background</span>=<span class="string">"@drawable/selector_item_pressed"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_gravity</span>=<span class="string">"end"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:gravity</span>=<span class="string">"center"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"></<span class="name">LinearLayout</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">com.chengww.fingerdemo.InputMethodView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/inputMethodView"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_alignParentBottom</span>=<span class="string">"true"</span> /></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">RelativeLayout</span>></span></span><br></pre></td></tr></table></figure><p><strong>密码显示圆点框</strong></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PwdView</span> <span class="keyword">extends</span> <span class="title">View</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> ArrayList<String> result;<span class="comment">//输入结果保存</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> count;<span class="comment">//密码位数</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> size;<span class="comment">//默认每一格的大小</span></span><br><span class="line"> <span class="keyword">private</span> Paint mBorderPaint;<span class="comment">//边界画笔</span></span><br><span class="line"> <span class="keyword">private</span> Paint mDotPaint;<span class="comment">//掩盖点的画笔</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> mBorderColor;<span class="comment">//边界颜色</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> mDotColor;<span class="comment">//掩盖点的颜色</span></span><br><span class="line"> <span class="keyword">private</span> RectF mRoundRect;<span class="comment">//外面的圆角矩形</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> mRoundRadius;<span class="comment">//圆角矩形的圆角程度</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">PwdView</span><span class="params">(Context context)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context);</span><br><span class="line"> init(<span class="keyword">null</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> InputCallBack inputCallBack;<span class="comment">//输入完成的回调</span></span><br><span class="line"> <span class="keyword">private</span> InputMethodView inputMethodView; <span class="comment">//输入键盘</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">InputCallBack</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onInputFinish</span><span class="params">(String result)</span></span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">PwdView</span><span class="params">(Context context, AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs);</span><br><span class="line"> init(attrs);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">PwdView</span><span class="params">(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyleAttr)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs, defStyleAttr);</span><br><span class="line"> init(attrs);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 初始化相关参数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">init</span><span class="params">(AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">float</span> dp = getResources().getDisplayMetrics().density;</span><br><span class="line"> <span class="keyword">this</span>.setFocusable(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">this</span>.setFocusableInTouchMode(<span class="keyword">true</span>);</span><br><span class="line"> result = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">if</span> (attrs != <span class="keyword">null</span>) {</span><br><span class="line"> TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.PwdView);</span><br><span class="line"> mBorderColor = ta.getColor(R.styleable.PwdView_border_color, Color.LTGRAY);</span><br><span class="line"> mDotColor = ta.getColor(R.styleable.PwdView_dot_color, Color.BLACK);</span><br><span class="line"> count = ta.getInt(R.styleable.PwdView_count, <span class="number">6</span>);</span><br><span class="line"> ta.recycle();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> mBorderColor = Color.LTGRAY;</span><br><span class="line"> mDotColor = Color.GRAY;</span><br><span class="line"> count = <span class="number">6</span>;<span class="comment">//默认6位密码</span></span><br><span class="line"> }</span><br><span class="line"> size = (<span class="keyword">int</span>) (dp * <span class="number">30</span>);<span class="comment">//默认30dp一格</span></span><br><span class="line"> <span class="comment">//color</span></span><br><span class="line"> mBorderPaint = <span class="keyword">new</span> Paint(Paint.ANTI_ALIAS_FLAG);</span><br><span class="line"> mBorderPaint.setStrokeWidth(<span class="number">3</span>);</span><br><span class="line"> mBorderPaint.setStyle(Paint.Style.STROKE);</span><br><span class="line"> mBorderPaint.setColor(mBorderColor);</span><br><span class="line"></span><br><span class="line"> mDotPaint = <span class="keyword">new</span> Paint(Paint.ANTI_ALIAS_FLAG);</span><br><span class="line"> mDotPaint.setStrokeWidth(<span class="number">3</span>);</span><br><span class="line"> mDotPaint.setStyle(Paint.Style.FILL);</span><br><span class="line"> mDotPaint.setColor(mDotColor);</span><br><span class="line"> mRoundRect = <span class="keyword">new</span> RectF();</span><br><span class="line"> mRoundRadius = (<span class="keyword">int</span>) (<span class="number">5</span> * dp);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onMeasure</span><span class="params">(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> w = measureWidth(widthMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> h = measureHeight(heightMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> wsize = MeasureSpec.getSize(widthMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> hsize = MeasureSpec.getSize(heightMeasureSpec);</span><br><span class="line"> <span class="comment">//宽度没指定,但高度指定</span></span><br><span class="line"> <span class="keyword">if</span> (w == -<span class="number">1</span>) {</span><br><span class="line"> <span class="keyword">if</span> (h != -<span class="number">1</span>) {</span><br><span class="line"> w = h * count;<span class="comment">//宽度=高*数量</span></span><br><span class="line"> size = h;</span><br><span class="line"> } <span class="keyword">else</span> {<span class="comment">//两个都不知道,默认宽高</span></span><br><span class="line"> w = size * count;</span><br><span class="line"> h = size;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {<span class="comment">//宽度已知</span></span><br><span class="line"> <span class="keyword">if</span> (h == -<span class="number">1</span>) {<span class="comment">//高度不知道</span></span><br><span class="line"> h = w / count;</span><br><span class="line"> size = h;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> setMeasuredDimension(Math.min(w, wsize), Math.min(h, hsize));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">measureWidth</span><span class="params">(<span class="keyword">int</span> widthMeasureSpec)</span> </span>{</span><br><span class="line"> <span class="comment">//宽度</span></span><br><span class="line"> <span class="keyword">int</span> wmode = MeasureSpec.getMode(widthMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> wsize = MeasureSpec.getSize(widthMeasureSpec);</span><br><span class="line"> <span class="keyword">if</span> (wmode == MeasureSpec.AT_MOST) {<span class="comment">//wrap_content</span></span><br><span class="line"> <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> wsize;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">measureHeight</span><span class="params">(<span class="keyword">int</span> heightMeasureSpec)</span> </span>{</span><br><span class="line"> <span class="comment">//高度</span></span><br><span class="line"> <span class="keyword">int</span> hmode = MeasureSpec.getMode(heightMeasureSpec);</span><br><span class="line"> <span class="keyword">int</span> hsize = MeasureSpec.getSize(heightMeasureSpec);</span><br><span class="line"> <span class="keyword">if</span> (hmode == MeasureSpec.AT_MOST) {<span class="comment">//wrap_content</span></span><br><span class="line"> <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> hsize;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">onTouchEvent</span><span class="params">(MotionEvent event)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (event.getAction() == MotionEvent.ACTION_DOWN) {<span class="comment">//点击控件弹出输入键盘</span></span><br><span class="line"> requestFocus();</span><br><span class="line"> inputMethodView.setVisibility(VISIBLE);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onFocusChanged</span><span class="params">(<span class="keyword">boolean</span> gainFocus, <span class="keyword">int</span> direction, Rect previouslyFocusedRect)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onFocusChanged(gainFocus, direction, previouslyFocusedRect);</span><br><span class="line"> <span class="keyword">if</span> (gainFocus) {</span><br><span class="line"> inputMethodView.setVisibility(VISIBLE);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> inputMethodView.setVisibility(GONE);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onDraw</span><span class="params">(Canvas canvas)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onDraw(canvas);</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> width = getWidth() - <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> height = getHeight() - <span class="number">2</span>;</span><br><span class="line"> <span class="comment">//先画个圆角矩形</span></span><br><span class="line"> mRoundRect.set(<span class="number">0</span>, <span class="number">0</span>, width, height);</span><br><span class="line"> canvas.drawRoundRect(mRoundRect, <span class="number">0</span>, <span class="number">0</span>, mBorderPaint);</span><br><span class="line"> <span class="comment">//画分割线</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i < count; i++) {</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> x = i * size;</span><br><span class="line"> canvas.drawLine(x, <span class="number">0</span>, x, height, mBorderPaint);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//画掩盖点,</span></span><br><span class="line"> <span class="comment">// 这是前面定义的变量 private ArrayList<Integer> result;//输入结果保存</span></span><br><span class="line"> <span class="keyword">int</span> dotRadius = size / <span class="number">8</span>;<span class="comment">//圆圈占格子的三分之一</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < result.size(); i++) {</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">float</span> x = (<span class="keyword">float</span>) (size * (i + <span class="number">0.5</span>));</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">float</span> y = size / <span class="number">2</span>;</span><br><span class="line"> canvas.drawCircle(x, y, dotRadius, mDotPaint);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">onCheckIsTextEditor</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> InputConnection <span class="title">onCreateInputConnection</span><span class="params">(EditorInfo outAttrs)</span> </span>{</span><br><span class="line"> outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;<span class="comment">//输入类型为数字</span></span><br><span class="line"> outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> MyInputConnection(<span class="keyword">this</span>, <span class="keyword">false</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setInputCallBack</span><span class="params">(InputCallBack inputCallBack)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.inputCallBack = inputCallBack;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">clearResult</span><span class="params">()</span> </span>{</span><br><span class="line"> result.clear();</span><br><span class="line"> invalidate();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="class"><span class="keyword">class</span> <span class="title">MyInputConnection</span> <span class="keyword">extends</span> <span class="title">BaseInputConnection</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">MyInputConnection</span><span class="params">(View targetView, <span class="keyword">boolean</span> fullEditor)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(targetView, fullEditor);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">commitText</span><span class="params">(CharSequence text, <span class="keyword">int</span> newCursorPosition)</span> </span>{</span><br><span class="line"> <span class="comment">//这里是接受输入法的文本的,我们只处理数字,所以什么操作都不做</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.commitText(text, newCursorPosition);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">deleteSurroundingText</span><span class="params">(<span class="keyword">int</span> beforeLength, <span class="keyword">int</span> afterLength)</span> </span>{</span><br><span class="line"> <span class="comment">//软键盘的删除键 DEL 无法直接监听,自己发送del事件</span></span><br><span class="line"> <span class="keyword">if</span> (beforeLength == <span class="number">1</span> && afterLength == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.sendKeyEvent(<span class="keyword">new</span> KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))</span><br><span class="line"> && <span class="keyword">super</span>.sendKeyEvent(<span class="keyword">new</span> KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.deleteSurroundingText(beforeLength, afterLength);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设置输入键盘view</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> inputMethodView</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setInputMethodView</span><span class="params">(InputMethodView inputMethodView)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.inputMethodView = inputMethodView;</span><br><span class="line"> <span class="keyword">this</span>.inputMethodView.setInputReceiver(<span class="keyword">new</span> InputMethodView.InputReceiver() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">receive</span><span class="params">(String num)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (num.equals(<span class="string">"-1"</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (!result.isEmpty()) {</span><br><span class="line"> result.remove(result.size() - <span class="number">1</span>);</span><br><span class="line"> invalidate();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (result.size() < count) {</span><br><span class="line"> result.add(num);</span><br><span class="line"> invalidate();</span><br><span class="line"> ensureFinishInput();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 判断是否输入完成,输入完成后调用callback</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">ensureFinishInput</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (result.size() == count && inputCallBack != <span class="keyword">null</span>) {<span class="comment">//输入完成</span></span><br><span class="line"> StringBuffer sb = <span class="keyword">new</span> StringBuffer();</span><br><span class="line"> <span class="keyword">for</span> (String i : result) {</span><br><span class="line"> sb.append(i);</span><br><span class="line"> }</span><br><span class="line"> inputCallBack.onInputFinish(sb.toString());</span><br><span class="line"> clearResult();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 获取输入文字</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">getInputText</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (result.size() == count) {</span><br><span class="line"> StringBuffer sb = <span class="keyword">new</span> StringBuffer();</span><br><span class="line"> <span class="keyword">for</span> (String i : result) {</span><br><span class="line"> sb.append(i);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> sb.toString();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>下方输入键盘</strong></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">InputMethodView</span> <span class="keyword">extends</span> <span class="title">LinearLayout</span> <span class="keyword">implements</span> <span class="title">View</span>.<span class="title">OnClickListener</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> InputReceiver inputReceiver;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">InputMethodView</span><span class="params">(Context context, @Nullable AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs);</span><br><span class="line"> LayoutInflater.from(context).inflate(R.layout.view_password_input, <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line"> initView();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">initView</span><span class="params">()</span> </span>{</span><br><span class="line"> findViewById(R.id.btn_1).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_2).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_3).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_4).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_5).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_6).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_7).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_8).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_9).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_0).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"> findViewById(R.id.btn_del).setOnClickListener(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line"> findViewById(R.id.layout_hide).setOnClickListener(<span class="keyword">new</span> OnClickListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onClick</span><span class="params">(View v)</span> </span>{</span><br><span class="line"> setVisibility(GONE);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onClick</span><span class="params">(View v)</span> </span>{</span><br><span class="line"> String num = (String) v.getTag();</span><br><span class="line"> <span class="keyword">this</span>.inputReceiver.receive(num);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设置接收器</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> receiver</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setInputReceiver</span><span class="params">(InputReceiver receiver)</span></span>{</span><br><span class="line"> <span class="keyword">this</span>.inputReceiver = receiver;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 输入接收器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">InputReceiver</span></span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">receive</span><span class="params">(String num)</span></span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>MainActivity 实现输入回调就可以得到回调结果了</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> <span class="keyword">extends</span> <span class="title">AppCompatActivity</span> <span class="keyword">implements</span> <span class="title">PwdView</span>.<span class="title">InputCallBack</span></span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onInputFinish</span><span class="params">(String result)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (result.equals(<span class="string">"123456"</span>)) {</span><br><span class="line"> fragment.dismiss();</span><br><span class="line"> Toast.makeText(<span class="keyword">this</span>, <span class="string">"验证成功"</span>, Toast.LENGTH_SHORT).show();</span><br><span class="line"> }<span class="keyword">else</span> {</span><br><span class="line"> showPwdError();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>今天暂时写这么多吧,整个项目还有点 BUG,标题说仿支付宝也仿的不像,<br>改天把后半部分整理出来修改下再发个完整版的。</p><p>源代码下载:<br><a href="http://git.oschina.net/chengww5217/fingerdemo" target="_blank" rel="noopener">http://git.oschina.net/chengww5217/fingerdemo</a></p><p>指纹解锁部分参考引用了以下文章,原作者指纹识别部分写的非常棒,强烈建议前往拜读:<br><a href="http://www.cnblogs.com/popfisher/p/6063835.html" target="_blank" rel="noopener">http://www.cnblogs.com/popfisher/p/6063835.html</a><br><a href="https://willowtreeapps.com/ideas/android-fingerprint-apis-an-overview-for-android-app-developers/" target="_blank" rel="noopener">https://willowtreeapps.com/ideas/android-fingerprint-apis-an-overview-for-android-app-developers/</a></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Android </category>
</categories>
<tags>
<tag> Android </tag>
<tag> FingerManager </tag>
<tag> 指纹识别 </tag>
</tags>
</entry>
<entry>
<title>AndroidStudio上传代码到码云(Oschina)教程</title>
<link href="/archives/Android_studio_push_to_oschina.html"/>
<url>/archives/Android_studio_push_to_oschina.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><h4 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h4><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/logo_gitee_light_cn_with_domain_name.png" alt="码云"></p><p>git 代码仓库一般是用的 github。但由于国内的情况,不进行科学上网的话那个速度简直是龟速。再加上公司那个 10M 的小水管实在是带不动,以及付费创建私有项目等,只好转战国内的代码托管站点。</p><p>我一直使用的都是 oschina,但是有个问题。Android studio 的 VCS 工具一直上传不了代码,总是被拒绝。今天终于搞清楚了是怎么回事,教程请往下看。</p><a id="more"></a><h4 id="教程"><a href="#教程" class="headerlink" title="教程"></a>教程</h4><p>1.首先前往码云<a href="https://git.oschina.net/" target="_blank" rel="noopener">注册账号</a>,没什么好说的,全中文的。<br>2.码云右上方加号,创建项目</p><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-project-create.png" alt="创建项目"></p><p>这个默认使用Readme初始化项目。如果你不使用任何文件初始化你的项目,即不勾选下图所有的复选框,那么就不会有冲突问题了。也就是和上传到GitHub一样,完全不会被拒绝。</p><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-01.png" alt=""></p><p>以下教程以使用 Readme 初始化项目为例,教大家如何将项目上传到码云,也同样适用于解决分支冲突的问题。</p><p>3.完成后,和上传到 github 步骤一样</p><ul><li>打开 Android Studio–VCS–Enable Version Control Integration…</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-02.png" alt="VCS"></p><ul><li>下拉选择 git</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-03.png" alt=""></p><ul><li>然后仓库就创建好了,此时左方文件应显示为红色</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-04.png" alt=""></p><ul><li>然后 VCS–git–add 代码添加到 git 仓库</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-05.png" alt=""></p><ul><li>有提示是否将 vcs.xml (版本控制的配置文件) 也一并加入到仓库,这个随意。</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-06.png" alt=""></p><ul><li>右上 commit changes</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-07.png" alt=""></p><ul><li>commit and push 或者 commit 然后再 push 也是一样</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-08.png" alt=""></p><ul><li>项目界面复制仓库地址,填写仓库地址,填写 oschina 用户名密码。第一次需要设定一个密码,以后无需登录,直接输入密码即可。然后 push 等待被拒绝</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-09.png" alt="仓库地址"><br><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-10.png" alt=""></p><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-11.png" alt="等待被拒绝提示"></p><ul><li>VCS–git–pull(看清楚不是 push)拉取 Readme.md<br>进行拉取 Readme.md 操作前,一定要把本地 git 仓库未 commit 的文件 commit。因进行 VCS 操作后,android studio 会自动添加 vcs.xml 等文件到 git。</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-12.png" alt=""></p><ul><li>刷新按钮,刷新出 master 勾选–pull</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-13.png" alt=""></p><p>如果出现提示 <code>fatal: refusing to merge unrelated histories</code> 不能合并不同的仓库的提示,请前往你项目的文件夹,右键 Git Base here.<br>输入 <code>git pull origin master --allow-unrelated-histories</code> 回车,等待合并拉取到Readme.md,关闭窗口。</p><ul><li>下方 VersionControl 可以看到 readme.md 已经被拉取</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-14.png" alt=""></p><ul><li>再次push就可以了</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-15.png" alt=""></p><ul><li>刷新oschina仓库地址,大功告成</li></ul><p><img src="https://img-cdn.pek3b.qingstor.com/Android_studio_push_to_oschina/gitee-step-16.png" alt=""></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Android </category>
</categories>
<tags>
<tag> Android </tag>
<tag> oschina </tag>
</tags>
</entry>
<entry>
<title>闪烁加载视图---ShimmerRecyclerView</title>
<link href="/archives/ShimmerRecyclerView.html"/>
<url>/archives/ShimmerRecyclerView.html</url>
<content type="html"><![CDATA[<!-- build time:Sat Mar 26 2022 19:45:54 GMT+0800 (China Standard Time) --><p>今天看到一个和支付宝加载RecyclerView显示加载中动画类似的库,感觉很有意思,参照官网说明写一个粗浅的使用说明给大家分享下。<br>具体的显示效果如下:</p><table><thead><tr><th>List Demo</th><th>Grid Demo</th></tr></thead><tbody><tr><td><img src="https://img-cdn.pek3b.qingstor.com/ShimmerRecyclerView/list-demo.gif" alt=""></td><td><img src="https://img-cdn.pek3b.qingstor.com/ShimmerRecyclerView/grid-demo.gif" alt=""></td></tr></tbody></table><a id="more"></a><h5 id="项目简介"><a href="#项目简介" class="headerlink" title="项目简介"></a>项目简介</h5><h6 id="ShimmerRecyclerView"><a href="#ShimmerRecyclerView" class="headerlink" title="ShimmerRecyclerView"></a>ShimmerRecyclerView</h6><p>一个自定义循环视图,带有shimmer视图,用来表现视图正在加载中。这个循环视图有一个内置的适配器来控制shimmer性能并提供两个方法:</p><ul><li>showShimmerAdapter() - 设置一个demo(加载中)适配器,显示预设子demo视图的数量。</li><li>hideShimmerAdapter() -隐藏加载中视图,恢复适配器以显示实际的子元素.</li></ul><h6 id="属性和方法"><a href="#属性和方法" class="headerlink" title="属性和方法"></a>属性和方法</h6><p>按如下属性和方法初始化demo视图.</p><table><thead><tr><th style="text-align:center">XML 属性</th><th style="text-align:center">Java 方法</th><th style="text-align:center">Explanation</th></tr></thead><tbody><tr><td style="text-align:center">app:demo_child_count</td><td style="text-align:center">setDemoChildCount(int)</td><td style="text-align:center">在shimmer适配器中展现设置demo视图的数量(Integer)。</td></tr><tr><td style="text-align:center">app:demo_layout</td><td style="text-align:center">setDemoLayoutReference(int)</td><td style="text-align:center">定义my_demo_view.xml ,请参考布局[1]。</td></tr><tr><td style="text-align:center">app:demo_layout_manager_type</td><td style="text-align:center">setDemoLayoutManager(LayoutManagerType)</td><td style="text-align:center">演示视图布局管理。</td></tr></tbody></table><p>[1] 一个显示List Demo的 my_demo_view.xml(加载中) 的例子:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="wrap_content"</span><br><span class="line"> android:layout_marginBottom="4dp"</span><br><span class="line"> android:layout_marginLeft="16dp"</span><br><span class="line"> android:layout_marginRight="16dp"</span><br><span class="line"> android:layout_marginTop="4dp"</span><br><span class="line"> android:background="@drawable/bg_card"</span><br><span class="line"> android:orientation="vertical" ></span><br><span class="line"></span><br><span class="line"> <LinearLayout</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="wrap_content"</span><br><span class="line"> android:layout_marginTop="16dp"</span><br><span class="line"> android:background="@android:color/white"</span><br><span class="line"> android:orientation="horizontal"</span><br><span class="line"> android:paddingLeft="8dp"</span><br><span class="line"> android:paddingRight="8dp"></span><br><span class="line"></span><br><span class="line"> <LinearLayout</span><br><span class="line"> android:layout_width="0dp"</span><br><span class="line"> android:layout_height="78dp"</span><br><span class="line"> android:layout_weight="1"</span><br><span class="line"> android:orientation="vertical"</span><br><span class="line"> android:paddingLeft="8dp" ></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> </LinearLayout></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="78dp"</span><br><span class="line"> android:layout_height="78dp"</span><br><span class="line"> android:layout_marginLeft="8dp"</span><br><span class="line"> android:layout_marginRight="8dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> </LinearLayout></span><br><span class="line"></span><br><span class="line"> <LinearLayout</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="wrap_content"</span><br><span class="line"> android:layout_marginBottom="16dp"</span><br><span class="line"> android:layout_marginLeft="16dp"</span><br><span class="line"> android:layout_marginRight="16dp"</span><br><span class="line"> android:orientation="vertical" ></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="6dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> <View</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="6dp"</span><br><span class="line"> android:layout_marginBottom="3dp"</span><br><span class="line"> android:layout_marginTop="3dp"</span><br><span class="line"> android:alpha="0.1"</span><br><span class="line"> android:background="@android:color/background_dark" /></span><br><span class="line"></span><br><span class="line"> </LinearLayout></span><br><span class="line"></span><br><span class="line"></LinearLayout></span><br></pre></td></tr></table></figure><p></p><p>[1.1] @drawable/bg_card:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><shape xmlns:android="http://schemas.android.com/apk/res/android"></span><br><span class="line"> <corners android:radius="3dp" /></span><br><span class="line"> <solid android:color="@android:color/white" /></span><br><span class="line"> <stroke</span><br><span class="line"> android:width="1dp"</span><br><span class="line"> android:color="#d6d6d6" /></span><br><span class="line"></shape></span><br></pre></td></tr></table></figure><p></p><h6 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h6><p>1.将本库添加到你的项目中<br>在你的项目级build.gradle文件中添加下列设置:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">allprojects {</span><br><span class="line"> repositories {</span><br><span class="line">...</span><br><span class="line">maven { url 'https://jitpack.io' }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>module下build.gradle文件中添加依赖:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> compile 'com.github.sharish:ShimmerRecyclerView:v1.0'</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>2.定义你的xml:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><com.cooltechworks.views.shimmer.ShimmerRecyclerView</span><br><span class="line">xmlns:app="http://schemas.android.com/apk/res-auto"</span><br><span class="line">android:id="@+id/shimmer_recycler_view"</span><br><span class="line">android:layout_width="match_parent"</span><br><span class="line">android:layout_height="match_parent"</span><br><span class="line">app:demo_child_count="10"</span><br><span class="line">app:demo_grid_child_count="2"</span><br><span class="line">app:demo_layout="@layout/layout_demo_grid"</span><br><span class="line">app:demo_layout_manager_type="grid" /></span><br></pre></td></tr></table></figure><p></p><p>@layout/layout_demo_grid指在加载spinner时展现的示例布局(参考[1])。<br>3.在你的Activity onCreate()方法中,初始化shimmer如下:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">ShimmerRecyclerView shimmerRecycler = (ShimmerRecyclerView) findViewById(R.id.shimmer_recycler_view);</span><br><span class="line">shimmerRecycler.setLayoutManager(layoutManager);</span><br><span class="line">shimmerRecycler.setAdapter(mAdapter);</span><br><span class="line">//显示加载中</span><br><span class="line">shimmerRecycler.showShimmerAdapter();</span><br></pre></td></tr></table></figure><p></p><p>4.耗时操作后显示真正要显示的内容,然后隐藏加载中:<br></p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">shimmerRecycler.hideShimmerAdapter();</span><br></pre></td></tr></table></figure><p></p><p>其余操作和RecyclerView一样,要完成上拉加载下拉刷新,点击事件等同RecyclerView可以在Adapter中完成。</p><p>项目官网地址:<a href="https://github.com/sharish/ShimmerRecyclerView" target="_blank" rel="noopener">https://github.com/sharish/ShimmerRecyclerView</a></p><!-- rebuild by neat -->]]></content>
<categories>
<category> Android </category>
</categories>
<tags>
<tag> Android </tag>
<tag> ShimmerRecyclerView </tag>
</tags>
</entry>
</search>