Giter Club home page Giter Club logo

swblog's People

Contributors

yunshuipiao avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

swblog's Issues

无序数组中的第k大元素-快速排序和堆排序

找到无序数组中的第k大元素, kth_Largest_element_in_an_array。
常见的解决方案有两种,快速排序**和堆排序**,简单记录一下过程,顺便复习快速排序和堆排序过程。

快速排序**

快速排序的理论基础是随机取一个数,大的放左边,小的放右边,完成一趟排序。
分别对左右两边递归做相同的分割,最后还剩一个数,自然有序,完成快速排序。
那么找第k大的元素,即完成一次排序后,随机找出的这个数的前面有 k -1个数,那么就是第k大个元素。
下标: k - 1 = index.
否则再根据下标关系判断在k在左边还是右边,继续如上操作。
快速排序完成一次排序的函数如下:

# 最后一个数为支点,从小到大排序
def partition_two(array, lo, hi):
    i = lo
    j = hi
    while i < j:
        while i < j and array[i] <= array[hi]:
            i += 1
        while i < j and array[j] >= array[hi]: # =保证支点不变,可以后续比较。
            j -= 1
        array[i], array[j] = array[j], array[i]
    array[i], array[hi] = array[hi], array[i]
    return i

下面得到分界点的下标,对左右两边递归分割,完成排序:

# 快速排序
def quick_sort(array, lo, hi):
    #参数合法性
    if array is None or lo >= hi:
        return
    mid = partition_two(array, lo, hi)
    quick_sort(array, lo, mid - 1)
    quick_sort(array, mid + 1, hi)

找第k大元素的代码类似。

堆排序**

首先堆不作介绍,分为大根堆和小根堆。堆的操作有插入和删除最大(小)元素,插入的节点放在最后,然后进行堆化操作。
堆的一个经典实现是完全二叉树,这样实现的堆为二叉堆。
下面是数组进行从小到大的堆排序过程。
###堆化数组
一个随机数组,对根节点i进行堆化操作(i的左右子树也都是堆)。
选择左右节点的小节点与根节点比较,若是根节点还小于较小值,则满足堆的性质。
否则进行交换,对交换过的节点继续进行堆化操作,直至全部堆化。

#对数组来说,以下标index为根节点的左右子树的下标为 index * 2 + 1, index * 2 +  2
# 将length个元素,i为节点的数组堆化(i的子节点也都是堆)
def big_heap_array(array, i, length):
    # 左叶子节点
    temp_value = array[i]
    node_index = (i << 1) + 1
    while node_index < length:
        #左右叶子节点的较大值
        if node_index + 1 < length and array[node_index + 1] > array[node_index]:
            node_index = node_index + 1
        # 如果根节点大于叶子节点,说明已经是堆,退出循环。
        if array[node_index] < temp_value:
            break
        array[i] = array[node_index]
        # 调整交换过元素的子节点
        i = node_index
        node_index = (i << 1) + 1
    array[i] = temp_value

上面操作是堆化i下标节点的数组,从小到大的排序过程如下:

  1. 从最后一个根节点开始,往上进行堆化, 此时array[0] 为最大值。
  2. 交换array[0]和最后一个元素,此时再进行堆化,得到最小的元素与倒数第二个元素,逐次完成从小到大排序。
#n长度的数组,最后一个根节点的下标为: n / 2 - 1
#构建最大堆,从小到大排序
def heap_sort_one(array):
    # 最后一个跟节点
    last_node_index = (len(array) >> 1) - 1
    for node_index in range(last_node_index, 0 - 1, -1):  #[llast_node_index .. 0] 的列表
        big_heap_array(array, node_index, len(array))
    # 堆顶最大值和最后一个数互换, 然后堆化数组
    for index in range(len(array) - 1, 0, -1):
        array[0], array[index] = array[index], array[0]
        big_heap_array(array, 0, index)

下面来解决此问题。
首先利用数组的前k个元素建立一个k大小的小根堆。逐次取后面的元素与堆顶比较, 如果大于堆顶,则替换堆顶,然后进行堆化。最终结果就是最大的前k个元素都在堆中,其中堆顶元素堆小,就是第k大的元素。

寻找的代码见github:https://github.com/yunshuipiao/sw-algorithms

react-native-+-mobx-入门到放弃

react-native + mobx 入门到放弃

标签(空格分隔): react mobx Android


作为一个刚开始看react-native的小白,找到的源码我都看不太懂,还有涉及redux的知识。后面同事介绍mobx,因此记录一下学习过程。

redux 和 mobx

过多的内容这里不做叙述,请看下面链接(可以知道是什么和为什么,很短)
如何理解 Facebook 的 flux 应用架构?
理解 React,但不理解 Redux,该如何通俗易懂的理解 Redux?
MobX vs Redux: Comparing the Opposing Paradigms - React Conf 2017 纪要

(对于redux,请参看Redux 入门教程(三):React-Redux 的用法

务必多看几遍,下面开始。

react-native

安装好所需的环境。
选择一个目录,执行

react-native init FirstReact
cd FisrtReact
npm install 
react-native run-adnroid

至此RN的demo可以正常启动。

mobx

安装mobx:
npm i mobx mobx-react --save
安装mobx相关的包

npm i babel-plugin-transform-decorators-legacy babel-preset-react-native-stage-0 --save-dev
安装一些 babel 插件,以支持 ES7 的 decorator 特性(后面有不用的方法, ES6)

然后打开 .babelrc 文件配置 babel 插件:

{
  "presets": ["react-native"],
  "plugins": [
    "syntax-decorators",
    "transform-decorators-legacy"  ]
}

依赖安装完成。

在根目录下创建mobxDemo文件夹。
新建AppState.js文件:

import {action, observable, useStrict} from "mobx";
import {extendObservable} from "mobx";

class AppState {
    @observable
    timer = 101;
    
    addTimers() {
        this.timer += 1
    }
    resetTimer() {
        this.timer = 0;
    }
}
export default new AppState()

@observable 指明需要观察的对象(值,列表,数组,类等。)
其他的 actioncomputed可以后面去了解。

同目录下新建文件:MobxDemo.js

@observer
class App extends React.Component {
    render() {
        return (
            <View>
                <Text>当前的数是:{AppState.timer}</Text>
                <Button
                    onPress={() =>
                        AppState.addTimers()}
                    title='add'
                />
                <Button
                    onPress={() =>
                        AppState.resetTimer()
                    }
                    title='reset'
                />
            </View>
        );
    }
}
export default App;

在需要观察的地方加@observer

end

修改index.js文件:

import { AppRegistry } from 'react-native';
import App from './mobx/MobxDemo';
AppRegistry.registerComponent('FirstReact', () => App);

刷新运行程序,完成对timer的加和重置。

ES6

在找资料的过程中,基本没有es6的相关实现。
中文文档:http://cn.mobx.js.org/

下面是ES6不带装饰器的写法:
AppState.js

import {action, observable, useStrict} from "mobx";
import {extendObservable} from "mobx";
class AppState {
    constructor() {
        let that = this;
        extendObservable(this, {
            timer: 11,
            get tenTimer() {
              return that.timer * 2
            },
            addTimers: action(function () {
                this.timer += 1
            }),
            resetTimer: action( () => {
                that.timer = 0
            })
        })
    }
}
export default new AppState()

MobxDemo.js

import React from "react";
import {observer} from "mobx-react";
import {View, Text, Button} from "react-native";
import AppState from './AppState'
const App = observer( class MobxDemo extends React.Component {
    render() {
        return (
            <View>
                <Text>当前的数是:{AppState.tenTimer}</Text>
                <Button
                    onPress={() =>
                        AppState.addTimers()}
                    title='add'
                />
                <Button
                    onPress={() =>
                        AppState.resetTimer()
                    }
                    title='reset'
                />
            </View>
        );
    }
})
export default App;

result: 统一数据处理,观察。

Android-AlertDialog-Rxjava2

自定义 AlertDialog 对话框

目前AlertDialog已经足够全面,包括简单,列表,多选列表对话框,可以满足大部分日常开发。
这里自定义一个对话框, 了解一下material design关于对话框的相关标准。

首先定义一个TextView的扩展方法(非常建议, 可以获取context), 如下:

fun TextView.countDown() {
    val frameLayout = FrameLayout(context)
    val rootView = LayoutInflater.from(context).inflate(R.layout.view_count_down, null)
    val mobileView = rootView.find<EditText>(R.id.vcd_et_mobile)
    val codeView = rootView.find<EditText>(R.id.vcd_et_code)
    val sendCode = rootView.find<TextView>(R.id.vcd_tv_send_code)
    sendCode.setOnClickListener {
        Timber.d("发送验证码")
    }

    val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.WRAP_CONTENT)
    lp.topMargin = dp2px(20f)
    lp.bottomMargin = dp2px(20f)
    lp.leftMargin = dp2px(24f)
    lp.rightMargin = dp2px(24f)
    rootView.layoutParams = lp
    frameLayout.addView(rootView)

    AlertDialog.Builder(context).setTitle("手机号码")
            .setNegativeButton("取消", { _, _ ->
                showToast("取消")
            })
            .setPositiveButton("确认", { _, _ ->
                showToast("确认")
            })
            .setView(frameLayout)
            .show()
}

布局UI如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/white">

    <EditText
        android:id="@+id/vcd_et_mobile"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:padding="10dp"
        android:hint="请输入手机"
        android:text=""
        android:textSize="16sp"
        />

    <EditText
        android:id="@+id/vcd_et_code"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="请输入验证码"
        app:layout_constraintTop_toBottomOf="@id/vcd_et_mobile"
        app:layout_constraintRight_toLeftOf="@id/vcd_tv_send_code"
        android:background="@color/white"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginTop="10dp"
        android:layout_marginRight="10dp"
        android:padding="10dp"
        android:textSize="16sp"
        />
    <TextView
        android:id="@+id/vcd_tv_send_code"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="@id/vcd_et_code"
        app:layout_constraintBottom_toBottomOf="@id/vcd_et_code"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@id/vcd_et_code"
        android:text="获取验证码"
        android:background="@color/white"
        android:textColor="@color/colorTextMain"
        android:textSize="16sp"
        android:gravity="center"
        />

</android.support.constraint.ConstraintLayout>

上面对应的几个点如下:

  1. 对话框的margin值
  2. 自定义对话框内容的字体大小:16sp
  3. 控件命名:文件名首写字母_控件名称缩写_所执行功能
  4. 相关控件的监听设置

完成的UI如下:

Rxjava2完成倒计时功能

假设对Rxjava2的使用有一定的了解,知道Observable, disposable, subscribe的简单使用。

首先定义一个Observabledisposable对象:

    var disposable: Disposable? = null
    val countTimesObservable = Observable.interval(1, TimeUnit.SECONDS)
            .take(10)
            .doOnDispose {
                Logger.d(TAG, "取消订阅")
            }
            .doOnSubscribe {
                Logger.d(TAG, "开始计时")
                disposable = it
            }.doOnComplete {
                Logger.d(TAG, "结束计时")
                sendCode.text = context.getString(R.string.login_get_code)
                disposable?.let {
                    if (it.isDisposed) {
                        it.dispose()
                    }
                }
            }
            .observeOn(AndroidSchedulers.mainThread())

这里简单解释一下几个方法的意思:
interval 表示每间隔1秒发送一个 Long 型数据;
take 表示取10个;
doOnDispose:表示当取消订阅(调用disposable.dispose())是产生的回调;
doOnSubscribe:表示开始接受数据(调用countTimesObservable.subscribe())是的回调,这里将disposable赋值,以便后续调用。
doOnComplete:表示结束时(倒计10s完成)的回调;
这里有个点需要注意doOnDispose 一定要在 doOnSubscribe 之前声明

这几个方法主要是做现场的保留与恢复,体现链式的好处。

接下面是完成按钮的监听,即点击 发送验证码 时的功能:

    sendCode.setOnClickListener {
        countTimesObservable.subscribe {
            val second = 9 - it
            sendCode.text = "已发送($second)"
            Timber.d(second.toString())
        }
    }

点击按钮开始倒计时,并修改显示文本,打印log如下:

04-14 14:50:57.915 28606-28606/com.swensun.swdesign D/ViewExtensionKt$countDown$countTimesObservable: 开始计时
04-14 14:50:58.922  D/ViewExtensionKt$countDown: 9
04-14 14:50:59.921  D/ViewExtensionKt$countDown: 8
04-14 14:51:00.921  D/ViewExtensionKt$countDown: 7
04-14 14:51:01.921  D/ViewExtensionKt$countDown: 6
04-14 14:51:02.919  D/ViewExtensionKt$countDown: 5
04-14 14:51:03.923  D/ViewExtensionKt$countDown: 4
04-14 14:51:04.922  D/ViewExtensionKt$countDown: 3
04-14 14:51:05.921  D/ViewExtensionKt$countDown: 2
04-14 14:51:06.922  D/ViewExtensionKt$countDown: 1
04-14 14:51:07.922  D/ViewExtensionKt$countDown$countTimesObservable: 结束计时
04-14 14:51:07.923  D/ViewExtensionKt$countDown: 0
04-14 14:51:07.924  D/ViewExtensionKt$countDown$countTimesObservable: 取消订阅

可以看到当点击按钮时开始倒计时,完成时调用onComplete完成现场恢复。

假如需要在倒计时中间停止怎么办:
设置dialog的监听, 取消订阅,修改代码如下:

    AlertDialog.Builder(context).setTitle("手机号码")
            .setNegativeButton("取消", { _, _ ->
                showToast("取消")
                disposable?.dispose()
            })
            .setPositiveButton("确认", { _, _ ->
                showToast("确认")
                disposable?.dispose()
            })
            .setView(frameLayout)
            .show()

开始计时后点击取消,log如下:

04-14 14:57:04.190 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown$countTimesObservable: 开始计时
04-14 14:57:05.198 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown: 9
04-14 14:57:06.198 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown: 8
04-14 14:57:07.197 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown: 7
04-14 14:57:08.197 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown: 6
04-14 14:57:09.116 28980-28980/com.swensun.swdesign D/ViewExtensionKt$countDown$countTimesObservable: 取消订阅

在倒计时到5时取消订阅,调用相关方法恢复现场。

这里作为一个Rxjava2的使用场景,相比使用Runnable来说,结构清晰明了,也是整个函数变的更纯,以后会陆续介绍使用Rxjava2的使用场景,方便开发。2

完成示例代码:
github:dialog countdown Rxjava2

关于纯函数,下面会介绍函数式编程的基本概念和方法, 以及柯里化的相关知识。

总结:

  1. 自定义AlertDialog 对话框
  2. Rxjava2 基本函数的使用

Android之纯函数-高阶函数简单介绍

说明:本文章在上篇的基础上,代码在Android的环境中运行,当然可以单独写。
后面的文章就不跟Android代码联系,最后再回到Android, 写一个柯里化和函数式编程的例子。

纯函数

上篇文章提到一下纯函数, 类似两个数的加法这样:

fun add1(a: Int, b: Int): Int {
    return a + b
}

纯函数可以理解为一种 相同的输入必定有相同的输出的函数,没有任何可以观察到副作用
纯函数的输出只取决于输入,输入定责输出定。在笔者写Android的过程中,尽量使用纯函数,较少副作用。

关于上面的函数,可以写成如下的形式:
fun add2(a: Int, b: Int) = a + b

高阶函数

在函数式编程中,首先要介绍一下 高阶函数

高阶函数是将函数用作参数或者返回值的函数。Kotlin的Collection类型中有大量的这种高阶函数,例如:Iterable的filter, foreach函数。本篇文章后面也可写相应的高阶函数。

将函数当做参数

现在开始,定义一个函数,将上面的add1当做参数执行计算。
代码如下:

fun calculate(a: Int, b: Int, f: (Int, Int) -> Int) : Int {
    return f(a, b)
}

如上, 定义了一个calculate函数, 该函数接收三个参数, 整形的a,b和作为函数的f。这比较特别,看参数定义可以知道,函数f将两个Int型值作为输入,输出一个Int值。可以进行如下调用:

codeBtn.setOnClickListener {
                when (position) {
                    0 -> {
                        codeBtn.countDown()
                    }
                    1 -> {
                        val result = calculate(2, 3, ::add1)
                        Timber.d("计算结果为: $result")
                    }
                }
            }

上述是RecyclerView的部分代码,这里只关注1的情况,将(2,3,::add1)传入,add1前有双冒号表示当做函数参数传入。输入结果如下:

 D/CodeExampleActivity$ItemViewHolder$bindData: 计算结果为: 5

当然也可以去除双冒号:既然函数可以当做参数或者返回值,那么相应的可以将函数理解为对象,
定义一个函数对象,执行如上相同的操作:

val add3 = {a: Int, b: Int -> a + b}
Timber.d(add3(3, 5).toString())
val result = calculate(2, 3, add3)
Timber.d("计算结果为: $result")

log打印如下:

 D/CodeExampleActivity$ItemViewHolder$bindData: 函数对象计算结果:8
 D/CodeExampleActivity$ItemViewHolder$bindData: 计算结果为: 5

将函数当做返回值

相比起来,我觉得当做返回值要难一些,慢慢来:

fun add4(): (Int, Int) -> Int {
    return ::add2
}

fun add5() = {
    add3
}

val add6 = add3

首先做如上函数声明,函数add2 和函数对象 add3 用作返回值。
因为add2是函数,所以有双冒号;add3是对象,特殊在其是一个函数。

那么怎么去调用呢:

                        val addF = add4()

                        val addFF = add5()
                        Timber.d("函数add4(): ${add4()}")
                        Timber.d("函数对象addF: $addF")

                        Timber.d("函数add5(): ${add5()}")

                        Timber.d("函数对象addF计算结果:${addF(1, 2)}")
                        Timber.d("函数add4()计算结果: ${add4()(3, 4)}")


                        Timber.d("函数addFF计算结果: ${addFF()(4, 5)}")
                        Timber.d("函数add5()计算结果: ${add5()()(6, 7)}")

log日志打印如下:

 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add4(): fun add2(kotlin.Int, kotlin.Int): kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数对象addF: fun add2(kotlin.Int, kotlin.Int): kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add5(): () -> (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数对象addF计算结果:3
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add4()计算结果: 7
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数addFF计算结果: 9
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add5()计算结果: 13

在以上kotlin代码中有一个小插曲:
log打印中出现提示: Kotlin reflection is not available
解决方案:kotlin-reflection-is-not-available
build.gradle 中添加 implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

对log输出稍做解释,不过我相信没有介绍也能看懂。
函数add4() 对象addF都是 函数add2(), log中有参数和返回值信息。
而add5()是对象add3, 是一个对象,接收0个参数返回一个函数,这个函数接收两个参数返回一个值。
根据log可以简单的了解其用法,后面会介绍。

进一步改写:

fun add7(): (Int, Int) -> Int {
    return fun(a: Int, b: Int): Int {
        return a + b
    }
}

fun add8(): (Int, Int) -> Int {
    return {a: Int, b: Int -> a + b}
}

val add9 = {a: Int, b: Int -> a + b}

这里我们直接定义匿名函数当做函数值。同样看一下log打印输出:

                        val addF = add7()

                        val addFF = add8()
                        Timber.d("函数add7(): ${add7()}")
                        Timber.d("函数对象addF: $addF")

                        Timber.d("函数add8(): ${add8()}")
                        Timber.d("函数addFF: $addFF")

                        Timber.d("函数add6(): $add9")

                        Timber.d("函数对象addF计算结果:${addF(1, 2)}")
                        Timber.d("函数add7()计算结果: ${add7()(3, 4)}")


                        Timber.d("函数addFF计算结果: ${addFF(4, 5)}")
                        Timber.d("函数add8()计算结果: ${add8()(6, 7)}")

log输出:

 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add7(): (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数对象addF: (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add8(): (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数addFF: (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add9: (kotlin.Int, kotlin.Int) -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数对象addF计算结果:3
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add7()计算结果: 7
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数addFF计算结果: 9
 D/CodeExampleActivity$ItemViewHolder$bindData: 函数add8()计算结果: 13

这里不做解释,相信你也能看懂了。

其中有个地方需要注意,即add5()函数的定义为什么是:
函数add5(): () -> (kotlin.Int, kotlin.Int) -> kotlin.Int:

这里我再写两个函数说明一下:

val add5_1 = { 10 }

val add5_2 = 10

fun add5_1() = {
    10
}

fun add5_2(): Int {
    return 10
}

这里要搞清楚对象和函数的区别就简单。
当返回或者等于{}时,它就是一个函数, 调用后返回最后一个对象。

                        val addF = add5_1
                        val addFF = add5_2
                        val addFFF = add5_1()
                        val addFFFF = add5_2()
                        Timber.d("$addF")
                        Timber.d("${addF()}")
                        Timber.d("$addFF")
                        Timber.d("$addFFF")
                        Timber.d("$addFFFF")

输出如下:

 D/CodeExampleActivity$ItemViewHolder$bindData: () -> kotlin.Int
 D/CodeExampleActivity$ItemViewHolder$bindData: 17
 D/CodeExampleActivity$ItemViewHolder$bindData: 10 D/CodeExampleActivity$ItemViewHolder$bindData: () -> kotlin.Int D/CodeExampleActivity$ItemViewHolder$bindData: 10

那么假如返回的最后一个不是数怎么办,当然,它可以是一个函数。
如下演示:

val add5_3_1 = {
    val temp = 10
    {a: Int -> a + temp}
}


fun add5_3_2() = {
    val temp = 10
    {a: Int -> a + temp}
}

首先是add5_3_2()是一个函数,其返回值也是一个函数,该函数完成+10的功能。
如下调用:

                        Timber.d("${add5_3_1()(15)}")
                        Timber.d("${add5_3_2()()(15)}")

现在应该能简单理解并知道结果为 25 了吧。

以上就是介绍将函数作为输入和输出的简单介绍。当然,我上面的例子由于过于浅显和简单,容易混淆,实际使用过程中也不会如上定义函数, 不过慢慢会理解的。
接下来就可以继续上述的一个事情,针对collection写自己的高阶函数。

下篇文章见

完整代码查看:
github: 纯函数 高阶函数 函数当做输入输出

docker创建带auth验证的mongodb数据库

记录本身,即已是反抗

首先下载mongo镜像,简单命令不做过多叙述,前面文章有介绍怎么基本使用docker。
之后编写docker-compose.yml文件,运行 docker-compose up -d生成容器并后台启动。

root创建

// docker-compose.yml
// command:启动是的命令行参数,添加认证auth
version: '2'
services:
  mongodb:
    image: mongo
    ports:
        - 27017:27017
    volumes:
        - "./data/configdb:/data/configdb"
        - "./data/db:/data/db"
    command: mongod --auth
    tty: true

运行docker ps查看容器是否运行。
image.png
进入docker容器并进入mongo命令行。

docker exec -it 4 /bin/bash
mongo

此时show dbs无法执行,需要认证。
切换到admin并创建root用户:

use admin
db.createUser({ user: 'root', pwd: 'root', roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] })

image.png
如上,可以看到root用户创建成功。exit退出mongo命令行,带验证的mongodb已经创建成功。

普通用户创建

接下来创建普通用户,并演示验证。
再次执行mongo 进入mongodb命令行。
image.png

可以看到root用户验证成功,并且可以查看数据库。

下面创建普通用户,和创建root用户基本一致,只是角色不同 。

//拥有对数据库app的读写权限。
use app
db.createUser(
  {
    user: "swen",
    pwd: "swen",
    roles: [ { role: "readWrite", db: "app" }
             ]
  }
)

image.png
创建成功并exit退出,swen用户可以对(只能对)app进行操作。
下面做基本演示。

基本验证:
image.png
演示往 test集合插入简单数据,并查看数据库状态。
image.png

介绍到此完毕。
参考资料:
MongoDB 用户名密码登录
MongoDB 常用基本命令

github: https://github.com/yunshuipiao

从谷歌官方例子看constraintlayout

从谷歌官方例子看constraintlayout

标签(空格分隔): googlesamples 知乎


研究官方文档,是通往业界大拿的途径-----不知道是谁。
这是一个新的系列,打算看一些 github 上谷歌官方的小例子,好处不必多说。
最近的项目中,所有的布局已经换成了 constraintlayout,这是谷歌推荐的布局。
因此打算从 constraintlayoutexamples 入手。

一点小技巧:视图编辑器用来辅助,布局还是得手写代码。其中 tools 开头的属性是负责在视图编辑器辅助展示我们的布局,手写布局可以忽略。

基本知识不做过多介绍,说一些我的收获。

  1. 关于 view 的居中:
    用的最多的是约定上下左右即可,通过 margin 设置偏移量。
    若是要求图中的 button2 相对 button1 的底部居中, 相对 centeredbutton 的右边居中:
    则设置 button2top, bottombutton1 top 对齐,右边类之。
    实现下图的居中对齐:
    中间按钮先居中对齐,然后是左右。

  2. layout_constraintDimensionRatio属性:
    设置空间的宽高比,默认宽:高。官方中可以通过指定:“h, 16:9” 设成高:宽(有待确认)。注意:不固定的一方的值设为0dp

  3. 关于guideline,特别好用,其中:
    app:layout_constraintGuide_percent:用百分比设置偏移量;
    app:layout_constraintGuide_begin: 具体数值设置偏移量。
    与之不同的属性:app:layout_constraintHorizontal_bias="0.5"
    通过百分比设置 view 的偏移量

  4. 链条(chains):相互引用, 可以设置偏移量,方便统一管理。
    其中 layout_constraintVertical_chainStyle 属性在链条的头部设置。
    三个值:
    packed:链条控件紧凑
    spread:链条控件均匀分布。
    spread_inside:链条内部控件均匀分布

  5. 通过 constraintset 设置动画
    参考资料constraintlayout动画

我的总结:不要盲目使用,根据需求综合使用 constraintlayout,搭配 linelayout
使用视图编辑器坐辅助,手写布局。

官方例子原址

欢迎加微信youquwen1226与我讨论。

2016.10(drawable, android初学, 生命周期,任务与返回栈, IPC)

笔者自16年毕业至如今,开发 ios 三个月,Android 七个月。自觉学习的知识严重不足,但偶尔记点开发的心得。因此打算整理一下这些笔记,月为单位,特此记录。

2016.7 -- 2016.9 ios开发时期三个月,暂且不表。

###2017.10
开始熟悉项目源码,几个学习的关键点:handlegradle, 匿名内部类和泛型, adapterLayoutinflater.inflate
其中项目用到的Kvo绑定是借鉴ios的一套框架,可以使用 databinding 加以替换。

反射机制, AppBarLayout

关于Android Drawable的微技巧阅读
几个要点:

  1. drawable文件夹存放图片文件,.png, .jpg. .9.png, selectorxml文件,而mipmap只用来存放应用程序的图标。
  2. 根据经验,公司的设计不可能每个尺度的图都能给到。图片最佳的放置文件夹是drawable-xxhdip
//判断2的幂次方
if ((n & -n) == n) // ture

后面阅读了stormzhang的博客,Android学习之路
里面涉及到的书籍推荐,第一行代码,疯狂Android讲义, Thinking In Java中文版,Effective java中文版,我也七七八八看过。此外,还涉及到android基础的文章推荐,Android activity生命周期, 四大组件的基本介绍,Intent, 屏幕适配。中级知识:viewGson, 布局优化(inculde重用,mergeviewstub), 异步消息处理。 进阶知识:gradle, 性能优化,以及兼容库,必备的开源库等,非常值得一看。

awesome-android-tips:常用的android代码。

有关生命周期:

onresumeactivity获得用户焦点,在与用户交互。
onstartactivity用户可见,包括有一个activtity或者view在其上面,但没有完全覆盖,用户可以看到但不能与其交互。

参考文章:Android任务和返回栈
有关任务栈:涉及到几种启动模式:
任务是一个antivity的集合, 使用栈来管理其中的activity, 也被成为返回栈(back task)。
当任务处于后台,返回栈中的所有activity都会进入停止状态。

默认的任务和activity的行为

  • activity A 启动 B时,A进入停止状态,系统保留相关信息(滚动位置,文本框输入内容等)。如果用户在B中按下back, A重新回到运行状态。
  • 当用户按Home, 该任进入后台,返回栈中所有activity进入停止状态而且状态被保留。重新打开时将后台任务直接到前台, 恢复最顶端activity
  • 当用户按下back, 最顶端的Activity从返回栈移除并被销毁,前面的activity处于栈顶并活动。
  • 每个activity都可以被实例化很多次,即使在不用的任务中。

通过在manifest文件设置元素的属性,或者在启动activity是配置Intent的flag来实现。
注意以下几个intentflag

FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_SINGLE_TOP

定义启动模式(两种方式):

使用manifest文件:launchMode属性来制定

standard:默认启动,每次启动创建实例,并放入当前任务栈。

singleTop:若将启动act在当前任务栈中存在且处于栈顶, 不会创建实例,而是调用栈顶actonNewIntent()方法。这种方式启动的act可被实例化多次,一个任务也可包含多个实例

singleTask:系统创建一个新的任务,并将此act放入新任务栈底。如果现有任务已存在该act的实例, 系统不会再创建,而是直接调用onNewIntent()。此模式在同一任务中只会存在一个实例。(此模式的启动act都是指其他程序的act, 才会创建新任务)

singleInstance:与singleTask类似,但不会向act所在的任务中再添加其他act

使用Intent flags
在startActivity时,为intent加入flag来改变act与任务的关联方式。
FLAG_ACTIVITY_NEW_TASK:新启动act被放入新任务(启动其他程序),模拟launcher
FLAG_ACTIVITY_CLEAR_TOP:若将启动act已存在,不会再创建,而是关闭此act之上的所有act
FLAG_ACTIVITY_SINGLE_TOP:与singletop的效果一样。

android的IPC:进程间通信,通过Bindersocket实现。

实现SerializablePracalable来持久化数据,完成对象的序列化,后者安卓主推,但是将对象序列化到存储设备或者对像序列化后通过网络传输稍显麻烦,所以选择使用。

Android之Kotlin高阶函数-柯里化-偏函数

自定义Collection的高阶函数

接上篇文章,其简单介绍了纯函数,高阶函数,写了几个简单将函数当做输入输出的例子。
现在来自定义我们的高阶函数。
首先来看最简单的forEach:

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)
    list.forEach { println(it) }
    // 1, 4, 7
}

看一下源码

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

简单解释:这里用到了泛型,简单忽略。
forEach中的action就是一个函数,类似我们上面的println()
函数执行也好理解:循环每一个元素执行action。

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)
    list.forEach { print1(it) }
    list.forEach { print2(it) }
    list.forEach({ print1(it) })
    list.forEach(print2)
    list.forEach(print3)
}

fun print1(a: Int) {
    print(a)
}

val print2 = { a: Int ->
    print(a)
}

val print3 = ::print1

上述5个forEach的输出相同,在上篇文章的基础上,相信理解不难。

接下来看一下:filter:

    val list = arrayListOf(1, 4, 7)
    list.filter { it != 1 }.forEach { println(it) }  // 4, 7
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

filter函数: 根据给定的高阶函数对元素进行判断后的Boolean返回值来过滤,返回新的迭代器。
稍微花5分钟理解一下。

接下来定义一个Collection的扩展函数,对每个元素指定 +2 功能:

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)
    list.add2().forEach { println(it) } // 3, 6, 9

}

fun Iterable<Int>.add2(): List<Int> {
    val tempList = arrayListOf<Int>()
    for (e in this) {
        tempList.add(e + 2)
    }
    return tempList
}

没有用到泛型, 非常容易理解,对Collection中的每个值+2。
当然,可以把2配置成参数:

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)
    list.add2(3).forEach { println(it) }

}

fun Iterable<Int>.add2(a: Int): List<Int> {
    val tempList = arrayListOf<Int>()
    for (e in this) {
        tempList.add(e + a)
    }
    return tempList
}

上面看过的源码中,inline是内联的意思,针对lambda优化,表示调用这个函数时,将代码复制到调用的位置,而不经过函数的调用栈, 节省开销。

下面在此基础上改一下,加上lambda。

inline fun Iterable<Int>.calculate(f: (Int) -> Int): List<Int> {
    val tempList = arrayListOf<Int>()
    for (e in this) {
        tempList.add(f(e))
    }
    return tempList
}

定义一个扩展函数,同样也是完成加2的功能:

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)
    list.calculate { add2(it) }.forEach { println(it) }
    list.calculate { add3(it) }.forEach { println(it) }

    list.calculate(::add2).forEach { println(it) }
    list.calculate(add3).forEach { println(it) }

    list.calculate { it + 2 }.forEach { println(it) }
}

fun add2(a: Int): Int {
    return a + 2
}

val add3 = {a: Int -> a + 2}

inline fun Iterable<Int>.calculate(f: (Int) -> Int): List<Int> {
    val tempList = arrayListOf<Int>()
    for (e in this) {
        tempList.add(f(e))
    }
    return tempList
}

上面有5中函数的调用演示,其结果都一样,现在应该非常容易理解。

回过头来看文章最前面定义的高阶函数:

fun Iterable<Int>.add2(): List<Int> {
    val tempList = arrayListOf<Int>()
    for (e in this) {
        tempList.add(e + 2)
    }
    return tempList
}

编译器有智能转换的功能, 转换后代码如下:

fun Iterable<Int>.add2(): List<Int> {
    val tempList = this.map { it + 2 }
    return tempList
}

此处看到了map,结合上面定义calculate函数,接收一个函数完成类似的功能,此时去看map的源码应该非常容易理解。
此时还可以继续只能转换:

fun Iterable<Int>.add2(): List<Int> {
    return map { it + 2 }
}

此时对比如下调用:

fun main(args: Array<String>) {
    val list = arrayListOf(1, 4, 7)

    list.add2().forEach { println(it) }
    list.map { it + 2 }.forEach { println(it) }
    list.map(::add2).forEach { println(it) }
}

fun add2(a :Int): Int {
    return a + 2
}

结果一样,不过过多解释。至此,相信应该不难写出自己的高阶函数。

柯里化

柯里化是一个数学概念, 简单的来说,就是对于一个有多个参数的函数,转换成每次只接受一个参数的函数,最后输入结果。
Currying 的重要意义在于可以把函数完全变成「接受一个参数;返回一个值」的固定形式。
看代码,从简单开始:

首先定义一个扩展方法执行打印输出:

fun <T> T?.println() = kotlin.io.println(this)

首先是一个加法函数:

fun main(args: Array<String>) {
    add(1, 2).println()
}

fun add(a: Int, b: Int): Int {
    return a + b
}

这是最简单的调用方法,柯里化后希望变成如下调用:
add(1)(2).println()
修改后的代码如下:

fun main(args: Array<String>) {
    add(1, 2).println()
    add1(1)(2).println()
    add2(1)(2).println()

    add3(1)(2).println()

}

fun add(a: Int, b: Int): Int {
    return a + b
}
fun add1(a: Int): (Int) -> Int {
    return {b: Int -> a + b}
}
fun add2(a: Int) = {b: Int -> a + b}

val add3 = {a: Int -> {b: Int -> a + b}}

全部打印的结果输出都是为3,其在函数和对象之间有略微的区别,也容易理解。

    val addOne = add1(1)
    addOne(2).println()
    addOne(4).println()

另外还可以如此调用, addOne是一个函数对象,接收一个函数完成+1的功能。

偏函数:对一个多参数的函数,通过指定其中的一部分参数后得到的仍然是一个函数,那么这个函数就是原函数的一个偏函数了。

偏函数与 Currying 有一些内在的联系,如果我们需要构造的偏函数的参数恰好处于原函数参数的最前面,那么我们是可以使用 Currying 的方法获得这一偏函数的;当然,如果我们希望得到任意位置的参数被指定后的偏函数,后面介绍方法。

加入我要完成3个参数的加法怎么办,简单写一下:

fun main(args: Array<String>) {

    add(1, 2, 3).println()
    add1(1)(2)(3).println()
}

fun add(a: Int, b: Int, c: Int): Int {
    return a + b + c
}

fun add1(a: Int): (Int) -> (Int) -> Int {
    return fun(b: Int): (Int) -> Int {
        return {c: Int -> a + b + c}
    }
}

如下,完成了3个参数相加的currying。加入随着参数的增多,会变得越来越复杂。
我们可以定义一个 函数的扩展函数来进行函数currying:

fun main(args: Array<String>) {
    ::add.curried()(1)(2)(3).println()   // 6
}

fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried()
        = fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = this(p1, p2, p3)

fun <P1, P2, R> Function2<P1, P2, R>.curried()
        = fun(p1: P1) = fun(p2: P2) = this(p1, p2)

fun add(a: Int, b: Int, c: Int): Int {
    return a + b + c
}

不容易理解, 看一下Function3的定义,

/**
 * A functional interface (callback) that computes a value based on multiple input values.
 * @param <T1> the first value type
 * @param <T2> the second value type
 * @param <T3> the third value type
 * @param <R> the result type
 */
public interface Function3<T1, T2, T3, R> {
    /**
     * Calculate a value based on the input values.
     * @param t1 the first value
     * @param t2 the second value
     * @param t3 the third value
     * @return the result value
     * @throws Exception on error
     */
    @NonNull
    R apply(@NonNull T1 t1, @NonNull T2 t2, @NonNull T3 t3) throws Exception;
}

这里不理解也没关系,记住currying怎么写,后面慢慢理解的。

偏函数

上面有简单提到, 如果我们希望得到任意位置的参数被指定后的偏函数,也需要定义 函数的扩展方法, 如下:

 fun <P1, P2, R> Function2<P1, P2, R>.partial1(p1: P1) = fun(p2: P2) = this(p1, p2) 
 fun <P1, P2, R> Function2<P1, P2, R>.partial2(p2: P2) = fun(p1: P1) = this(p1, p2)

目前kotlin标准库中没有实心,所以这里要定义两个方法,这两个方法分别用来生成两个参数分别被指定后的偏函数。

如下使用:

fun main(args: Array<String>) {
    val addOne = add.partial1(1)
    val addTwo = add.partial2("2")
    addOne("2").println()
    addTwo(3).println()
}

val add = {a:Int, b: String -> "$a$b"}

非常容易的能看出区别

至此,函数柯里化和偏函数基本介绍完了,本来还想写一个Android的具体例子, 限于篇幅,后面再补。

总结:

  1. 函数柯里化
  2. 偏函数

本篇文章和上一篇结合紧密,建议一起阅读。

快上车,scrapy爬虫飙车找福利(三)

前面文章讲到怎么提取动态网页的全部内容。接下来返回文章一,怎么登录并且保存登录状态,以便带上cookies下次访问。

步骤

  1. 利用selenium登录知乎, 登录成功后保存cookies 到本地。
  2. 请求之前读取cookies, 加载cookies访问,看是否成功登录。

详细步骤:

  1. 利用selenium登录知乎
    回到文章一, 从自从有了知乎,再也不用找福利了……链接开始。
    从提取标题开始:
    image.png
if __name__ == '__main__':
   url = 'https://www.zhihu.com/collection/146079773'
   res = requests.get(url, verify=False)
   resSoup = BeautifulSoup(res.content, 'lxml')
   items = resSoup.select("div > h2 > a")
   print(len(items))

verify=False:取消ssl的验证。
运行这段代码, 输出结果未0, 粘贴该网页到一个没有登录知乎的浏览器打开,重定向到登录页, 说明需要登录。

验证:

if __name__ == '__main__':
    url = 'https://www.zhihu.com/collection/146079773'
    # res = requests.get(url, verify=False)
    driver = webdriver.Chrome()
    driver.get(url)
    driver.implicitly_wait(2)
    res = driver.page_source
    resSoup = BeautifulSoup(res, 'lxml')
    items = resSoup.select("div > h2 > a")
    print(len(items))

执行代码,打开浏览器,显示知乎登录页,说明访问收藏夹需要登录。
image.png

登录技巧:
使用selenium打开登录页,设定延时时间(比如60s),手动输入账号密码登录知乎,60秒之后保存cookies到本地,完成登录。后续请求携带保存的cookie进行的登录。如果cookies过期,则简单重复这一步骤。
下面是详细步骤:


if __name__ == '__main__':

    ssl._create_default_https_context = ssl._create_unverified_context
    # url = 'https://www.zhihu.com/collection/146079773'
    url = "https://www.zhihu.com/signin"
    # res = requests.get(url, verify=False)
    driver = webdriver.Chrome()
    driver.implicitly_wait(5)
    driver.get(url)
    time.sleep(40)
    cookies = driver.get_cookies()
    pickle.dump(cookies, open("cookies.pkl", "wb"))
    print("save suc")

执行这段代码,看是否有cookies.pkl文件生成, 成功保存了cookies。

接下来用第二段代码去验证。

if __name__ == '__main__':
    cookies = pickle.load(open("cookies.pkl", "rb"))
    url = 'https://www.zhihu.com/collection/146079773'
    driver = webdriver.Chrome()
    driver.get("https://www.zhihu.com/signin")
    for cookie in cookies:
        print(cookie)
        driver.add_cookie(cookie)
    driver.get(url)
    driver.implicitly_wait(2)
    res = driver.page_source
    resSoup = BeautifulSoup(res, 'lxml')
    items = resSoup.select("div > h2 > a")
    print(len(items))

打开浏览器, 加载任意网页,接着加载cookies, 打开给定的url。运行代码,
image.png
如上,看到打印的cookies和提取的10个标题, 打开浏览器,页面不是登录页,说明登录成功。看cookies的有效时间。即可知道下次cookies的替换时间。

至此,最难定义的动态网页和登录问题已经解决。
下面就是怎么保存抓到的数据。
我的想法是先将需要登录的10页中所有问题和问题链接提取出来,保存为json文件以后后续处理。接着对每一个问题下的所有图片链接提取,保存或者直接下载就看个人选择了。

  1. 提取该收藏夹下的全部链接保存到为json文件或者txt文件。
    回到爬虫,现在我们已经有了cookies,可以不用selenium很快的保存问题列表。
    将上一步保存的cookies.pkl复制一份到根目录,或者配置打开属性。
    首先取消settings.py 文件中的中间键,
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    # 'zhihu.middlewares.PhantomJSMiddleware': 100,
}

反爬虫策略:
对于访问过快,网页一般会静止访问或者直接封ip。因此对于需要登录的爬虫来说,限制访问速度,比如5秒/次, 或者每个ip每分钟最大访问次数。对于不需要登录的页面来说,使用代理ip是最好的选择,或者降低访问次数都是可行的办法。
settings.py 文件的设置,

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# CONCURRENT_REQUESTS = 32

# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 2
# The download delay setting will honor only one of:
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16

这几个选项都是控制访问速度的,一般我设置DOWNLOAD_DELAY即可,即每两秒访问一次。

执行代码如下:

class Zhihu(scrapy.Spider):
    name = "zhihu"
    cookeis = pickle.load(open("cookies.pkl", "rb"))
    urls = []
    questions_url = set()
    for i in range(1, 11):
        temp_url = "https://www.zhihu.com/collection/146079773?page=" + str(i)
        urls.append(temp_url)

    def start_requests(self):
        for url in self.urls:
            request = scrapy.Request(url=url, callback=self.parse, cookies=self.cookeis)
            yield request

    def parse(self, response):
        print(response.url)
        resSoup = BeautifulSoup(response.body, 'lxml')
        items = resSoup.select("div > h2 > a")
        print(len(items))
        for item in items:
            print(item['href'])
            self.questions_url.add(item['href'] + "\n")

    @classmethod
    # 信号的使用
    def from_crawler(cls, crawler, *args, **kwargs):
        print("from_crawler")
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_closed)
        return s

    def spider_opened(self, spider):
        print("spider close, save urls")
        with open("urls.txt", "w") as f:
            for url in self.questions_url:
                f.write(url)

命令行运行爬虫,查看url.txt文件。
image.png

可以看到,成功抓取了44个链接,去除people, zhuanlan等几个无效链接,
后面即可从该文件读取内容,拼接链接,利用selenium做中间键提取所有的图片链接。

总结:这本文章讲了如何利用selenium去手动登录网站,保存cookies,以后后续登录(几乎可以登录所有的网站,限制访问速度避免被封)。

这三篇文章讲解了怎么使用scrapy去抓取想要的东西。现在无需使用框架,也可以涉及实现自己的爬虫。对于怎么保存图片,使用代理,后面会做简单介绍。
后面会写一篇怎么将爬虫部署在服务器上,利用docker搭建python环境去执行爬虫。

weixin:youquwen1226
github:https://github.com/yunshuipiao
欢迎来信探讨。

关于android architecture Component的入门资料(二)

上篇回顾:kotlin和android architecture Component,Room的完成与测试。

接上篇,如果看到模拟的数据插入,那么可以继续往下看

lifecycle

该架构提供了一种管理activity和fragmeng生命周期的方法,并且让接下来介绍的livedata可以在有效生命周期内收到观察,更新UI。首先来看:
因为还是测试版的原因,目前可以继承lifecycleActivity和lifecyclefragment来实现。

class StoryListFragment : LifecycleFragment() {}

说一下类的继承结构:
lifecycleActivity(LifecycleFragment)实现LifecycleRegistryOwner接口。只有一个方法:

//源码
public interface LifecycleRegistryOwner extends LifecycleOwner {
    @Override
    LifecycleRegistry getLifecycle();
}

在activity或fragmeng中可以调用getLifecycle(),可以得到LifecycleRegistry,获取当前ui的状态。
可以做个测试:
自定义一个MainObserve:

class MainObserve : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    internal fun start() {
        Log.d(TAG, "start: " + "init")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    internal fun create() {
        Log.d(TAG, "create: " + "init")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    internal fun stop() {
        Log.d(TAG, "stop: " + "init")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    internal fun destory() {
        Log.d(TAG, "destory: " + "init")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    internal fun resume() {
        Log.d(TAG, "resume: " + "init")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    internal fun pause() {
        Log.d(TAG, "pause: " + "init")
    }
    companion object {
        private val TAG = "MyObserve"
    }
}

然后在ui中使用lifeRegister.addObserver(MainObserve())可以得到结果。
推荐看一下这几个接口和类的源代码,都非常简单。

上面都不是重点。

Lifecycle最佳实践

  • 尽可能保持UI控制器(Activity和Fragment)的简洁。它们不应该去获取数据,而是使用ViewModel 来做这个工作,然后观察LiveData 把变化反应给view。
  • 尝试写数据驱动的UI,UI controller的职责是在数据改变的时候更新view,或者把用户的操作通知给ViewModel。
  • 把数据逻辑放在ViewModel 类中。ViewModel的角色是UI控制器与app其余部分的桥梁。不过要注意,ViewModel的职责并不是获取数据(比如,从网络)。相反 ViewModel应该调用合适的控件去做这件事情,然后把结果提供给UI控制器。
  • 使用 Data Binding来让view和UI控制器之间的接口保持干净。这可以让你的view更加声明式同时最小化Activity和Fragment中的代码。如果你更喜欢用Java代码做这件事情,使用 Butter Knife来避免繁琐的代码。
  • 如果你的UI非常复杂,考虑创建一个Presenter类来处理UI的变动。通常这会有点多余,但可能会让UI的测试更加简单。
  • 绝对不要在 ViewModel中引用View 或者 Activity 的context。如果ViewModel活的比Activity更长,Activity可能会泄漏,无法正常回收。

ViewModel

该架构与其他架构类似,提供viewmodel来分离UI和数据的处理。之前有部分的数据在UI中处理,UI层会变得臃肿,并且难以修改和重构。
以我的例子说明:
自定义的viewmodel需要继承ViewModel。如果需要用到application做系统相关的事情,可以继承AndroidViewModel(也可以用全局单例提供)。

class StoryListViewModel(val app: Application): AndroidViewModel(app) {}

这时,可以在activity使用var viewModel = ViewModelProviders.of(activity).get(StoryListViewModel::class.java)得到viewmodel的实例,处理数据,联系UI。
注意:经笔者测试,如果使用var viewModel = StoryListViewModel()new 出实例也是可以的,此时还可以带参数,方便很多。按我的理解,区别在于:
当然:viewModel不推荐带参数
前者可以保持一个单例,使用情况多个fragmeng在同一个activity中,利用viewmodel可以保存数据,多个fragmeng共享数据。但是,参数app必须是Application,在viewModel中做强制转换。
(app as ZhiJokeApplication).如果是ZhiJokeApplication,编译会错误。

Livedata

livedata和viewmodel和结合在一起的。livedata时一个可以感受UI生命周期的组件,不会造成内存泄漏。根据最佳实践,viewmodel处理数据逻辑,但并不涉及数据获取(db, net)。该架构推荐repository来获取保存数据。
与上一篇文章结合,自定义一个repository:

//使用了Dagger2, 不懂可以跳过。后面会写文章介绍。
//简单理解;等同于new, 创建实例
class StoryRepository @Inject
constructor(var db: AppDatabase) {

    companion object {
        var TAG = "StoryRepository"
    }

    fun insertStories(storys: List<Story>) {
        Flowable.just(storys)
                .observeOn(Schedulers.io())
                .subscribe {
                    db.beginTransaction()
                    try {
                        db.storyDao().insertStories(storys)
                        db.setTransactionSuccessful()
                    } finally {
                        db.endTransaction()
                    }
                }
    }
    fun loadAllStories(): LiveData<List<Story>> {
        var result = db.storyDao().loadAllStoriesLiveData()
        return  result
    }

    fun simlutateInsertData() {
        var list = ArrayList<Story>()

        for (i in 1..20) {
            var s = Story()
            s.id = (i)
            s.data = ((i * i).toString() + "--" + i.toString())
            s.displayData = ((i * i).toString() + "--" + i.toString())
            s.title = ((i * i).toString() + "--" + i.toString())
            s.image = ((i * i).toString() + "--" + i.toString())
            list.add(s)
        }
        insertStories(list)
    }
}

该类有三个方法,模拟插入数据和读取数据。

在viewmodel定义以下数据:

var storyList = MediatorLiveData<List<Story>>()

@Inject
//实例化StoryRepository
lateinit var storyRes: StoryRepository

关于livedata:有三个类,

  • livedata抽象类:可以被viewmodel观察,在数据变化时响应。
  • MutableLiveData:继承livedata, 提供setvalue和postvalue,设置值。
  • MediatorLiveData: 可以对livedata进行观察,作出响应(有例子介绍)

我的理解:
之所以声明为MediatorLiveData,是因为story的来源可以有多种,网络,数据库。如果livedata的话,则网络获取的数据就不能赋值给livedata。如果MutableLiveData,普通list可以赋值给MutableLiveData。此时有一个问题,若数据库有变(插入数据), 则loadAllStories的返回结果时livedata,无法通过
MutableLiveData.postvalue赋值, 因为livedata.getvalue的值永远为null。
因此,viewModel的整体代码如下:

class StoryListViewModel(val app: Application): AndroidViewModel(app) {
    companion object {
        val TAG = "StoryListViewModel"
    }
    var storyList = MediatorLiveData<List<Story>>()

    @Inject
    lateinit var storyRes: StoryRepository
    init {
        (app as ZhiJokeApplication).component.inject(this)
    }
    fun getStoryList():LiveData<List<Story>> {
        storyList.addSource(storyRes.loadAllStories(), {
            if (it == null || it.size == 0) {
                storyRes.simlutateInsertData()
                Thread.sleep(1000)
            }
            storyList.postValue(it)
        })
        return storyList
    }
}

getStoryList提供给UI,对storyList进行观察,根据变化去更新UI。

var viewModel = ViewModelProviders.of(activity).get(StoryListViewModel::class.java)
        viewModel.getStoryList().observe(this, Observer {
            if (it != null) {
            // update UI
                mAdapter.setStories(it)
            }
        })

到此为止,改架构大部分都介绍完了,感觉也没介绍的多详细可以自己实现一个demo感受一下。

我的kotlin版demo

几点建议:
使用dagger2来解耦。
如果rxjava2使用的非常熟练的话,也可以不用livedata。

这两篇文章也是对自己学习的一个回顾,其中穿插有自己对某些点的感想。
希望得到各位的建议。(wx:youquwen1226,欢迎交流)

后面打算写篇文章介绍dagger2.
若是不习惯用kotlin,我会再上传一个java版本的demo。

快上车,scrapy爬虫飙车找福利(一)

以前也有写过爬虫,抓过网易云歌单和豆瓣读书的数据,当时有两个问题解决的不够好, 自动化和登录。最近花时间用scrapy去写,自认为更好的解决了上述问题。这篇文章当作一个记录,也可当作学习教程(需要BeautifulSoup, selenium基本知识)。

目标

用scrapy去抓取自从有了知乎,再也不用找福利了……收藏夹下每一个答案下的全部图片。

简易步骤

  1. 账号登录知乎,抓取全部答案的链接(去除重复文章,大概39个答案)。
{'url': '/question/36007260', 'title': '女生坚持健身是种什么样的体验?', 'dec': ['健身']}
{'url': '/question/22132862', 'title': '女生如何选购适合自己的泳装?', 'dec': ['泳装']}
{'url': '/question/22918070', 'title': '女生如何健身锻造好身材?', 'dec': ['健身']}
{'url': '/question/24214727', 'title': '大胸妹子如何挑选合身又好看的比基尼?', 'dec': [ '比基尼']}
{'url': '/question/263451180', 'title': '你觉得健身时哪个训练动作最酷炫?', 'dec': ['健身']}
{'url': '/question/28586345', 'title': '有马甲线是种怎样的体验?', 'dec': ['马甲线']}
{'url': '/question/68734869', 'title': '2017 年,你解锁了哪些运动技能?可以用「视频」展示么?', 'dec': ['解锁']}
{'url': '/question/31983868', 'title': '有什么好看的泳装推荐?', 'dec': ['泳装']}

如上,对每一个问题提取url, 标题和关键字,保存到json文件方便后续工作。

  1. 对每一个答案,抓取该答案下所有图片链接, 保存或者下载(此处用到selenium)。
  2. 结果:半天时间抓去图片20000+张, 部分如下:
    屏幕快照 2017-12-23 23.18.04.png

详细步骤

一. 先从2开始,目标:如何拍好私房照?链接下的所有图片。

  1. 新建工程 :scrapy start zhihu
    简单介绍一下,工程目录:
    image.png
    zhihu/spiders:爬虫的主要文件。
    zhihu/items.py:需要抓取的数据结构
    zhihu/middlewares.py:中间键文件,selenium处理动态网页。
    zhihu/pipelines.py:保存items中定义的数据结构或者下载图片(处理item)。

其余文件都是额外生成,不属于爬虫目录。
cookies.pkl:保存登录的cookies, 下次登录。
questions.json: 保存所有问题的链接,方便后续使用。
上面两个文件都是在第一步用到, 后续再讲。

  1. 最简单的爬虫
    相信看到这的童鞋都有用过requests库, BeautifulSoup去写过简单的爬虫。
    这里不做讨论。
    zhihu/spiders下新建zhihu.py文件,从这里开始。
import scrapy
class Zhihu(scrapy.Spider):
    name = "zhihu"
    urls = ["https://www.zhihu.com/question/22856657"]
    yield request

    def start_requests(self):
        for url in self.urls:
            request = scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        print(response.url)
        print(response.body)

name定义了爬虫的名字,urls定义需要爬取的链接,从start_requests开始,yield对每一个url执行得到生成器, scrapy经过网络请求返回后调用parse函数。
接下来在项目目录执行scrapy crawl zhihu 启动爬虫,看输出结果。
image.png
可以看到输出的url和html代码,最简单的爬虫执行完毕。
关键:该开始运行一定要日志输出。
image.png
遇到上述问题,需要打开settings文件做如下设置:

#重试设置
RETRY_ENABLE = False

# 日志输出
LOG_ENABLED = True
LOG_LEVEL = "INFO"

取消失败重试,设置日志级别和是否输出(对爬取无影响)。

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}

下载中间键中取消默认的UserAgent设置,以。及对robos.txt的设置。

  1. 提取图片链接。(BeautifulSoup)
    关于BeautifulSoup的使用可以看官方文档,简单明了。
    获取图片的css selector,提取图片链接。
    打开该url, 右击任何一张图片,检查即可看到该图片的位置。
    image.png

image.png
如上所示,即可找到该img的位置。
接下来看代码:

import scrapy
from bs4 import BeautifulSoup

class Zhihu(scrapy.Spider):
    name = "zhihu"
    urls = ["https://www.zhihu.com/question/22856657"]


    def start_requests(self):
        for url in self.urls:
            request = scrapy.Request(url=url, callback=self.parse)
            yield request

    def parse(self, response):
        print(response.url)
        resSoup = BeautifulSoup(response.body, 'lxml')
        items = resSoup.select("figure")
        print(len(items))
        for item in items:
            print(item)
            print(item.img)
            print(item.img['data-original'])

parse函数中,使用BeautifulSoup对网页分析。
结果如下:
image.png
对比输出,共计找到30个figure标签。
分别对figure,figure的子标签img 及其data-original属性进行输出。
粘贴属性到浏览器打开即可看到图片。
到此为止, 对如何拍好私房照?链接第一页的图片基本抓取完成。后面介绍怎么使用selenium对该链接下所有图片进行抓取。

有疑问请加weixin:youquwen1226,一起探讨。
github:https://github.com/yunshuipiao

快上车,scrapy爬虫飙车找福利(二)

上一篇文章实现的最简单的爬虫,抓取了某个链接下第一次加载的所有图片链接。因为存在下拉刷新, 因此怎么获得该页面的全部答案是这篇文章需要去处理的事情。

方案:

  1. 抓包,看下拉刷新向服务器发送什么请求,模拟去发送请求(结构化数据适用)
  2. selenium执行js的滑动到底部,判断是否滑动到底部,以此循环。

具体实施;

这里选择使用方案2,方案1后面遇到再讨论。

一:selenium的简单使用。
这里涉及selenium的安装,Selenium with Python官方文档讲解的特别简单。我使用的的chrome(可以配置无头属性)。
注意:需要将下载的driver配置环境变量,以便可以访问。

if __name__ == '__main__':
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    # driver = webdriver.Chrome(options=options)
    driver = webdriver.Chrome()
    driver.implicitly_wait(2)
    driver.get("https://www.zhihu.com/question/22856657")
    time.sleep(2)
    resSoup = BeautifulSoup(driver.page_source, 'lxml')
    items = resSoup.select("figure > span > div")
    print(len(items))
    for item in items:
        print(item)
    #driver.close()

在项目执行代码可以看到输出:python zhihu/spiders/zhihu.py
image.png
可以看到共抓到198张图片,对去data-src属性即可得图片链接。
代码解释:
前三行用于启动一个无头的driver。如果需要查看加载的情况,只用第四行代码即可,执行完毕可以查看浏览器打开的url, 如下。
image.png

接下来三行:
第一行启动driver的隐式等待,简单意思就是:2秒内网页加载完毕就往下执行,否则就加载完2秒,继续往下执行。
第二行用于打开链接,相当于手动在地址栏输入链接。
第三行延时,等待网页加载。
后面的内容前面有接触。
如果输出结果和我的相差不大,那么继续下一步。

二:selenium执行js代码,加载全部内容。

if __name__ == '__main__':
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    # driver = webdriver.Chrome(options=options)
    driver = webdriver.Chrome()
    driver.implicitly_wait(2)
    driver.get("https://www.zhihu.com/question/22856657")
    time.sleep(2)

    count = 1
    css_selector = "#root > div > main > div > div.Question-main > div.Question-mainColumn > div > div.Card > button"
    css_selector2 = "#root > div > main > div > div.Question-main > div.Question-mainColumn > div > div.CollapsedAnswers-bar"
    while len(driver.find_elements_by_css_selector(css_selector)) == 0 and \
            len(driver.find_elements_by_css_selector(css_selector2)) == 0:
        print("count:" + str(count))
        js = "var q=document.documentElement.scrollTop=" + str(count * 200000)
        count += 1
        driver.execute_script(js)
        time.sleep(0.5)

    resSoup = BeautifulSoup(driver.page_source, 'lxml')
    items = resSoup.select("figure > span > div")
    print(len(items))
    for item in items:
        print(item)

image.png
结果输出:共计抓取翻页13次,抓取662个图片链接。
中间部分新增的代码, count用于记录翻页次数。
css_selector和css_selector2用于判断某个元素是否存在,决定是否滑动到底部,如下。
image.png

用一个循环去执行js代码,简单意思是滑动到距离页面顶部x的距离。经过测试,200000/页是比较好的选择。

至此,可以抓取某个链接下的所有图片。

三: selenium与spider middlewares的结合。
上面一切顺利之后, 接下来去使双方结合。
关于scrapy 的下载中间键(DOWNLOADER_MIDDLEWARES):
简单来说,该中间键就是调用process_request, 将获取url的request经过处理,返回request,response,None三值之一。
返回 request:继续执行后面的process_request方法(包括中间键)
response:不知行后面的process_request方法,以此response结果直接返回,执行zhihu/spiders/zhihu.py 的回调方法。
具体请看官方文档: https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
(还有spider middlewares, 本次未用到)
image.png

话不多说,开始写代码:
在middlewares.py中定义自己的中间键:


class PhantomJSMiddleware(object):

    def __init__(self):
        options = webdriver.ChromeOptions()
        options.add_argument('headless')
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(1)

    def process_request(self, request, spider):
        print(request.url)
        driver = self.nextPage(request)
        return HtmlResponse(url=request.url, body=driver.page_source, encoding="utf-8")
        # 翻页操作

    def nextPage(self, request):
        self.driver.get(request.url)
        time.sleep(2)
        count = 1
        css_selector = "#root > div > main > div > div.Question-main > div.Question-mainColumn > div > div.Card > button"
        css_selector2 = "#root > div > main > div > div.Question-main > div.Question-mainColumn > div > div.CollapsedAnswers-bar"
        # css_selector = "div > a > img"
        # print(len(self.driver.find_elements_by_css_selector(css_selector)))
        while len(self.driver.find_elements_by_css_selector(css_selector)) == 0 and len(
                self.driver.find_elements_by_css_selector(css_selector2)) == 0:
            print("count:" + str(count))
            js = "var q=document.documentElement.scrollTop=" + str(count * 200000)
            count += 1
            self.driver.execute_script(js)
            time.sleep(0.5)
        print(count)
        time.sleep(2)
        return self.driver

    @classmethod
    # 信号的使用
    def from_crawler(cls, crawler):
        print("from_crawler")
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_closed)
        return s

    def spider_opened(self, spider):
        print("spider close")
        self.driver.close()

稍作解释:
__init__函数做初始化工作,
nextPage函数根据得到的request做翻页操作。
**process_request **函数是中间键的必要函数, 启动中间键之后,yield生成器中的request都会经过该函数,然后返回结果(一定要在此函数执行return)。

后面是spiders信号的使用实例, 用于在spiders执行结束的时候做处理工作,比如关闭driver等操作。

setttings.py:配置对下载中间键的使用。

DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'zhihu.middlewares.PhantomJSMiddleware': 100,
}

以上配置完毕,即可执行爬虫。
命令行执行 scrapy crawl zhihu启动爬虫,注意看日志,有如下输出。
image.png

下拉刷新根据网速决定,所以count值会有不同。
可以看到这里抓取到了660张图片链接(允许个别误差)。

至此,对于使用scrapy结合selnium抓取动态网页已经不是问题。

对于某些需要登录的链接,打开url之后会直接去到登录页。下一篇文章介绍怎么使用selenium 去登录,保存cookies, 带着cookies去请求(可能是万能的登录方法,对于图片验证, 手机验证码也可能适用)。

微信:youquwen1226
github:https://github.com/yunshuipiao
欢迎来信一起探讨。

mark一下

以后会在上面记录一下个人的学习心得,努力

关于android 悬浮窗和自启动的设置, 以及获取系统的信息

关于android 悬浮窗和自启动的设置, 以及获取系统的信息

标签(空格分隔):Android


悬浮窗

对于是否有开悬浮窗,程序是可以检测到的。
权限声明:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

权限检查:

对于android6.0 以上的机型来说,google将悬浮窗权限和其他危险权限单独列出,因此可以检测, 方法:

fun checkFloatWindowPermission(): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val result = Settings.canDrawOverlays(BaseContext.application)
        return result
    } else {
        //6.0 以下
        val result = checkRomFloatWindowPermission()
        return result
    }
    return true
}

android 4.4以下大部分机型声明即获取(包括大部分国产机), 因此只需判断4.4.4 -- 5.1之间的系统即可。

//如下,经过我的测试,只有小米和魅族, 华为需要在`api<=19`时单独处理,其他正常。
fun checkRomFloatWindowPermission(): Boolean {

    val brand = Build.MANUFACTURER
    if (brand.contains("Xiaomi")) {
        //check miui permission
        return checkMeizuAndMIUIFloatWindowPermission()
    }
    if (brand.contains("Huawei") || brand.contains("HUAWEI")) {
        return checkMeizuAndMIUIFloatWindowPermission()
    }
    if (Build.VERSION.SDK_INT <= 19) {
        return true
    }
    when (brand) {
        "Meizu" -> return checkMeizuAndMIUIFloatWindowPermission()
        "OPPO" -> return true
        "vivo" -> return true
        "Sony" -> return true
        "Letv" -> return true
        "LG" -> return true
    }
    return true
}
//-------------- Meizu MIUI huawei start---------------------

fun checkMeizuAndMIUIFloatWindowPermission(): Boolean {
    if (Build.VERSION.SDK_INT >= 19) {
        return checkMeizuAndMIUIOp(24)
    }
    return true
}

fun checkMeizuAndMIUIOp(op: Int): Boolean {
    if (Build.VERSION.SDK_INT >= 19) {
        val opsManager = BaseContext.application.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        try {
            val clazz = AppOpsManager::class.java
            val method = clazz.getDeclaredMethod("checkOp",
                    Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java)
            return AppOpsManager.MODE_ALLOWED == method.invoke(opsManager, op, Binder.getCallingUid(), BaseContext.application.packageName)
        } catch (e: Exception) {
            Logger.d(TAG, "checkOp", e)
        }
    } else {
        Logger.d(TAG, "Below API 19 cannot invoke!")
    }
    return false
}

//-------------- Meizu MIUI end ---------------------

至此,权限检查完毕。
不能动态获取,只能跳转到这只页去手动开启。

fun applyFloatWindowPermission(context: Context) {
    if (Build.VERSION.SDK_INT >= 23) {
        applySystemPermission(context)
        return
    }
    val brand = Build.MANUFACTURER
    when (brand) {
        "Huawei", "HUAWEI" -> applyHuaweiPermission(context)
        "Xiaomi" -> applyMIUIPermSetting(context)
        "Meizu" -> applyMeizuPermission(context)
        "OPPO" -> openOppoPermSetting(context)
        "vivo" -> openVivoPermSetting(context)
        else -> applySystemPermission(context)
    }
}

//-------------- Sysytem start ---------------------

fun  applySystemPermission(activity: Activity) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
    intent.data = Uri.parse("package:" + activity.packageName)
    activity.startActivityForResult(intent, 1)

}

//-------------- Sysytem end ---------------------
//-------------- Meizu start---------------------
fun applyMeizuPermission(activity: Activity) {
    val intent = Intent("com.meizu.safe.security.SHOW_APPSEC")
    intent.setClassName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity")
    intent.putExtra("packageName", activity.packageName)
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    activity.startActivity(intent)
}

//-------------- Meizu end ---------------------
//-------------- MIUI start---------------------
fun applyMIUIPermSetting(activity: Activity) {
    openMIUIPermSetting(activity)
}

fun openMIUIPermSetting(context: Context): Boolean {
    val i = Intent("miui.intent.action.APP_PERM_EDITOR")
    val componentName = ComponentName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity")
    i.component = componentName
    i.putExtra("extra_pkgname", context.packageName)
    try {
        context.startActivity(i)
    } catch (e: Exception) {
        Logger.e(TAG, "openMIUIPermSetting jump e:" + e.message)
        openSystemSetting(context)
        return false
    }
    return true
}
//-------------- MIUI start---------------------
//-------------- huawei start---------------------

fun applyHuaweiPermission(context: Context) {
    try {
        val intent = Intent()
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        var comp = ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity")//悬浮窗管理页面
        intent.component = comp
        val version = getEmuiVersion()
        Logger.d(TAG, "applyHuaweiPermission:"  + version)
        if (version == 3.0) {
            comp = ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity")//悬浮窗管理页面
            intent.component = comp
            context.startActivity(intent)
            return
        }
        context.startActivity(intent)
//        if (version == 3.1) {
//            //emui 3.1 的适配
//            context.startActivity(intent)
//        } else {
//            //emui 3.0 的适配
//            comp = ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity")//悬浮窗管理页面
//            intent.component = comp
//            context.startActivity(intent)
//        }
    } catch (e: Exception) {
        //抛出异常时提示信息
        Toast.makeText(context, "进入设置页面失败,请手动设置", Toast.LENGTH_LONG).show()
        Log.e(TAG, Log.getStackTraceString(e))
    }
}

fun getEmuiVersion(): Double {
    try {
        val emuiVersion = getSystemProperty("ro.build.version.emui") ?: return 4.0
        val version = emuiVersion.substring(emuiVersion.indexOf("_") + 1)
        return java.lang.Double.parseDouble(version)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return 4.0
}
//-------------- huawei end---------------------

附: 各手机厂商跳转到设置页的方法:(context)

// open app setting according to rom ----------------

fun applyBrandStrategy(activity: Activity) {
    val brand = Build.MANUFACTURER
    when (brand) {
        "Huawei", "HUAWEI" -> openHuaweiPermSetting(activity)
        "Xiaomi" -> openMIUIPermSetting(activity)
        "Meizu" -> openMeizuPermSetting(activity)
        "OPPO" -> openOppoPermSetting(activity)
        "vivo" -> openVivoPermSetting(activity)
        "Sony" -> openSonyPermSetting(activity)
        "Letv" -> openLetvPermSetting(activity)
        "LG" -> openLGPermSetting(activity)
        else -> openSystemSetting(activity)
    }
}

fun openMIUIPermSetting(activity: Activity): Boolean {
    val i = Intent("miui.intent.action.APP_PERM_EDITOR")
    val componentName = ComponentName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity")
    i.component = componentName
    i.putExtra("extra_pkgname", activity.packageName)
    try {
        activity.startActivity(i)
    } catch (e: Exception) {
        Logger.e(TAG, "openMIUIPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openMeizuPermSetting(activity: Activity): Boolean {
    val intent = Intent("com.meizu.safe.security.SHOW_APPSEC")
    intent.addCategory(Intent.CATEGORY_DEFAULT)
    intent.putExtra("packageName", activity.packageName)
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openMeizuPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openHuaweiPermSetting(activity: Activity): Boolean {
    val intent = Intent()
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    var comp = ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openHuaweiPermSetting jump e:" + e.message)
        comp = ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.SingleAppActivity")
        intent.component = comp
        try {
            activity.startActivity(intent)
        } catch (e1: Exception) {
            Logger.e(TAG, "openHuaweiPermSetting jump e1:" + e.message)
            openSystemSetting(activity)
            return false
        }
    }

    return true
}

fun openOppoPermSetting(activity: Activity): Boolean {
    val intent = Intent()
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    val comp = ComponentName("com.oppo.safe", "com.oppo.safe.permission.PermissionSettingsActivity")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openOppoPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openVivoPermSetting(activity: Activity): Boolean {
    val intent = Intent()
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    val comp = ComponentName("com.iqoo.secure", "com.iqoo.secure.MainActivity")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openVivoPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openSonyPermSetting(activity: Activity): Boolean {
    val intent = Intent()
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    val comp = ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openSonyPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openLGPermSetting(activity: Activity): Boolean {
    val intent = Intent("android.intent.action.MAIN")
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    val comp = ComponentName("com.android.settings", "com.android.settings.Settings\$AccessLockSummaryActivity")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openLGPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openLetvPermSetting(activity: Activity): Boolean {
    val intent = Intent()
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.putExtra("packageName", activity.packageName)
    val comp = ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps")
    intent.component = comp
    try {
        activity.startActivity(intent)
    } catch (e: Exception) {
        Logger.e(TAG, "openLetvPermSetting jump e:" + e.message)
        openSystemSetting(activity)
        return false
    }

    return true
}

fun openSystemSetting(activity: Activity): Boolean {
    val localIntent = Intent()
    localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    localIntent.action = "android.settings.APPLICATION_DETAILS_SETTINGS"
    localIntent.data = Uri.fromParts("package", activity.packageName, null)
    try {
        activity.startActivity(localIntent)
    } catch (e: Exception) {
        Logger.e(TAG, "openSystemSetting jump e:" + e.message)
        return false
    }

    return true
}

开启自启动

自启动比较简单,android系统无法检测到是否开启,一般做法是首次安装提示用户跳转设置:

    private fun applySelfStartSetting() {

        var intent = Intent()
        try {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            var componentName: ComponentName? = null
            val brand = Build.MANUFACTURER
            when (brand) {
                "Xiaomi" -> {
                    componentName = ComponentName("com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity")
                }
                "samsung", "Samsung" -> {
                    componentName = ComponentName("com.samsung.android.sm" ,"com.samsung.android.sm.app.dashboard.SmartManagerDashBoardActivity")
                }
                "Huawei", "HUAWEI" -> {
                    componentName = ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity")
                }
                "Meizu" -> {
                    componentName = ComponentName.unflattenFromString("com.meizu.safe/.permission.SmartBGActivity")
                }
                "OPPO" -> {
                    componentName = ComponentName.unflattenFromString("com.oppo.safe/.permission.startup.StartupAppListActivity")
                }
                "vivo" -> {
                    componentName = ComponentName.unflattenFromString("com.iqoo.secure/.safeguard.PurviewTabActivity")
                }
                "Letv" -> {
                    intent.action = "com.letv.android.permissionautoboot"
                }
                else -> {
                    intent.action = "android.settings.APPLICATION_DETAILS_SETTINGS"
                    intent.data = Uri.fromParts("package", packageName, null)
                }
            }
            intent.component = componentName
            startActivity(intent)
        } catch (e: Exception) {
            Logger.d(PhoneDialogActivity.TAG, "applySelfStartSetting:"  + e)
            intent = Intent(Settings.ACTION_SETTINGS)
            startActivity(intent)
        }
    }

android 相关信息的获取方法:

1.获取手机型号(Build)

所属包: android.os.Build
作用(含义): 从系统属性中提取设备硬件和版本信息
静态属性:

1. BOARD 主板:The name of the underlying board, like goldfish. 
2.  BOOTLOADER 系统启动程序版本号:The system bootloader version number. 
3. BRAND 系统定制商:The consumer-visible brand with which the product/hardware will be associated, if any. 
4. CPU_ABI cpu指令集:The name of the instruction set (CPU type + ABI convention) of native code. 
5. CPU_ABI2 cpu指令集2:The name of the second instruction set (CPU type + ABI convention) of native code. 
6. DEVICE 设备参数:The name of the industrial design. 
7. DISPLAY 显示屏参数:A build ID string meant for displaying to the user 
8. FINGERPRINT 唯一识别码:A string that uniquely identifies this build. Do not attempt to parse this value. 
9. HARDWARE 硬件名称:The name of the hardware (from the kernel command line or /proc). 
10. HOST 
11. ID 修订版本列表:Either a changelist number, or a label like M4-rc20. 
12. MANUFACTURER 硬件制造商:The manufacturer of the product/hardware.(我们目前只需要关注这个静态属性即可)
13. MODEL 版本即最终用户可见的名称:The end-user-visible name for the end product. 
14. PRODUCT 整个产品的名称:The name of the overall product. 
15. RADIO 无线电固件版本:The radio firmware version number. 在API14后已过时。使用 getRadioVersion()代替。 
16. SERIAL 硬件序列号:A hardware serial number, if available. Alphanumeric only, case-insensitive. 
17. TAGS 描述build的标签,如未签名,debug等等。:Comma-separated tags describing the build, like unsigned,debug. 
18. TIME 
19. TYPE build的类型:The type of build, like user or eng. 
20. USER

打开其他应用程序中的Activity或服务(ComponentName)

所属包: android.content.ComponentName
构造方法使用方式如下:

  1. 传递当前上下文和将要跳转的类名;
  2. 传递一个String包名和String类名;
  3. 传递一个Parcel数据容器。
    需要关注的方法:unflattenFromString(“传递将要跳转的地址,格式为包名/跳转Activity Name”)

通过adb获取跳转包名路径

adb为我们提供了一个可以打印出当前系统所有service信息,在后面可加上具体的服务名的命令

adb shell dumpsys

获取设备电池信息:adb shell dumpsys battery
获取cpu信息:adb shell dumpsys cpuinfo
获取内存信息:adb shell dumpsys meminfo 
要获取具体应用的内存信息,可加上包名 
adb shell dumpsys meminfo PACKAGE_NAME
获取Activity信息:adb shell dumpsys activity
获取package信息:adb shell dumpsys package 
加上-h可以获取帮助信息 
获取某个包的信息:adb shell dumpsys package PACKAGE_NAME
获取通知信息:adb shell dumpsys notification
获取wifi信息:adb shell dumpsys wifi 
可以获取到当前连接的wifi名、搜索到的wifi列表、wifi强度等
获取电源管理信息:adb shell dumpsys power 
可以获取到是否处于锁屏状态:mWakefulness=Asleep或者mScreenOn=false
获取电话信息:adb shell dumpsys telephony.registry 
可以获取到电话状态,例如mCallState值为0,表示待机状态、1表示来电未接听状态、2表示电话占线状态 
mCallForwarding=false #是否启用呼叫转移 
mDataConnectionState=2 #0:无数据连接 1:正在创建数据连接 2:已连接mDataConnectionPossible=true #是否有数据连接mDataConnectionApn= #APN名称等
adb shell dumpsys activity top
//获取当前显示activity的详细信息,包括所属包名和activity名,activity的布局信息等等。

参考资料 : http://www.voidcn.com/article/p-dpiicqfm-zw.html


Android之监听来电,权限管理, 多语言方案,双卡拨号

有关权限管理

Anroid6.0以下, 权限申明后即可获取(国产定制系统除外)
在manifest文件里声明权限:

//电话相关权限
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>

6.0以上不仅要声明,还需运行时获取。
步骤大概如下:

已打电话权限为例说明(一般情况):

    private void checkAndRquestCallPermission() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "checkCallPermission: " + "没有打电话权限");
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);// 1:code
        }
    }
    //重载activity的onRequestPermissionsResult方法
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 1) {
            if (1 == grantResults.length && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
                Log.d(TAG, "onRequestPermissionsResult: " + "已获取:" + permissions[0] + " 权限");
            }     
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

对shouldShowRequestPermissionRationale方法的补充说明:
在请求权限时,这个函数用来可以用来给用户提示为什么需要权限(用户没有勾选不再提示按钮,第一次请求或是拒绝后返回ture):

    private void checkAndRquestCallPermission() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "checkCallPermission: " + "没有电话权限");
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
                Log.d(TAG, "checkCallPermission: " + "need phone permission");
                //弹出对话框,提示用户允许或者拒绝,允许则请求权限
            }
            //此处默认允许
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);// 1:code
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 1) {
            if (1 == grantResults.length && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
                Log.d(TAG, "onRequestPermissionsResult: " + "以获取:" + permissions[0] + " 权限");
            }
            else {
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
//                  //用户没有勾选不再提示,并拒绝
                    Log.d(TAG, "onRequestPermissionsResult: " + "refuse one time");
                } else {
//                    用户勾选提示并拒绝
                    Log.d(TAG, "onRequestPermissionsResult: " + "refuse forever");
                    //跳转权限设置页
                }
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

根据测试,小米机型貌似没有可勾选功能,一次拒绝默认永远拒绝。下次申请到权限设置页。
(国产机型区分对待)
具体代码参考
https://github.com/yunshuipiao/SWBase/blob/Sbranch/app/src/main/java/com/macmini/swensun/swbase/MainActivity.java

来电监听和拨打监听

简单介绍此功能的实现
在有电话权限的前提下
在有电话权限的前提下

功能比较简单,看代码就行:
继承BroadcastReceiver类处理即可:

public class PhoneReceiver extends BroadcastReceiver {
    private static final String TAG = "PhoneReceiver";
    private boolean mInComingFlag = false;
    private static String ACTION_NEW_INCOMMING_CALL = "android.intent.action.PHONE_STATE";
    private PhoneStateListener listen = new PhoneStateListener(){
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            super.onCallStateChanged(state, incomingNumber);
            switch (state) {
                case TelephonyManager.CALL_STATE_RINGING:
                    //电话打进响铃中,电话打出没有此状态
                    mInComingFlag = true;
                    Log.d(TAG, "onCallStateChanged: " + incomingNumber);
                    break;
                case TelephonyManager.CALL_STATE_IDLE:
                    //挂断电话
                    if (mInComingFlag) {
                        Log.d(TAG, "onCallStateChanged: " + "call hang up");
                    }
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    //电话接听
                    if (mInComingFlag) {
                        Log.d(TAG, "onCallStateChanged: " + "接听电话:" + incomingNumber);
                    }
            }
        }
    };

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
            //电话打出
            mInComingFlag = false;
            String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
            Log.d(TAG, "onReceive: " + "call phoneNumber:" + phoneNumber);
        }
        if (intent.getAction().equals(ACTION_NEW_INCOMMING_CALL)) {
            //电话打进
            TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            tm.listen(listen, PhoneStateListener.LISTEN_CALL_STATE);

        }
    }
}

接着在manifest声明该receiver和需要过滤的广播

        <receiver android:name=".phone.PhoneReceiver"
                  android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
                <action android:name="android.intent.action.PHONE_STATE"/>
                <action android:name="android.intent.action.MY_SELF_RECEIVER"/>
            </intent-filter>
        </receiver>

自定义广播发送并接收(同上)

                //发送自定义广播,PhoneReceiver接收
                Intent intent = new Intent(ACTION);
                intent.putExtra("Msg", "helloReceiver");
                sendBroadcast(intent);

完整代码参考
https://github.com/yunshuipiao/SWBase/blob/Sbranch/app/src/main/java/com/macmini/swensun/swbase/phone/PhoneReceiver.java

多语言方案实现

    //选择语言并保存状态
    private void changeLanguage() {
        //弹出对话框或者其他方式选择语言,并持久化保存到本地
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setSingleChoiceItems(new String[]{"auto", "English", "简体中文"},
                getSharedPreferences("language", Context.MODE_PRIVATE).getInt("language", 0),
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        SharedPreferences preferences = getSharedPreferences("language", Context.MODE_PRIVATE);
                        SharedPreferences.Editor editor = preferences.edit();
                        Log.d(TAG, "onClick: " + which);
                        //保存设置
                        editor.putInt("language", which);
                        editor.apply();
                        dialog.dismiss();

                        Intent intent = new Intent(MultiLanguageActivity.this, MultiLanguageActivity.class);
                        //重新打开一个返回栈并清除前者
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                        startActivity(intent);
                    }
                });
        AlertDialog dialog = builder.create();
        dialog.show();
    }
    //加载所保存的语言
    private void setLanguage() {
        SharedPreferences preferences = getSharedPreferences("language", Context.MODE_PRIVATE);
        int language = preferences.getInt("language", 0);

        Resources resources  = getResources();
        DisplayMetrics dispalyMetRics = resources.getDisplayMetrics();
        Configuration configuration = resources.getConfiguration();
        switch (language) {
            case 0:
                configuration.setLocale(Locale.getDefault());
                break;
            case 1:
                configuration.setLocale(Locale.ENGLISH);
                break;
            case 2:
                configuration.setLocale(Locale.CHINESE);
                break;
        }
        // FIXME: 2017/6/5
        resources.updateConfiguration(configuration, dispalyMetRics);
    }

添加语言文件strings:

因为对ActionBar不起作用,因此需要调用以下方法设置title显示。

getSupportActionBar().setTitle(R.string.app_name);

完整代码参考
https://github.com/yunshuipiao/SWBase/blob/Sbranch/app/src/main/java/com/macmini/swensun/swbase/Language/MultiLanguageActivity.java

双卡双待手机拨打电话(暂无监听双卡双待电话接听情况)

判断当前sim使用情况:

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
    private int checkDualSim() {
        int simNumber = 0;
        SubscriptionManager sm = SubscriptionManager.from(this);
        List<SubscriptionInfo> subs = sm.getActiveSubscriptionInfoList();
        if (subs == null) {
            d(TAG, "checkDualSim: " + "no sim");
            return simNumber;
        }
        if (subs.size() > 1) {
            simNumber = 2;
            d(TAG, "checkDualSim: " + "two sims");
        } else {
            d(TAG, "checkDualSim: " + "one sim");
            simNumber = 1;
        }
        for (SubscriptionInfo s: subs) {
            d(TAG, "checkDualSim: " + "simInfo:" + subs.toString());
        }
        return simNumber;
    }
//根据上述情况,初始化UI和拨打电话,尤其注意sim2的打电话情况
    @RequiresApi(api = Build.VERSION_CODES.M)
    private void callPhone(boolean isDualSim) {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "callPhone: " + "no call phone permission");
            return;
        }
        String phoneNumber = mBinding.etPhoneNumber.getText().toString().trim();
        phoneNumber = TextUtils.isEmpty(phoneNumber) ? "13422284669" : phoneNumber;
        if (!isDualSim) {
            //单卡
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:" + phoneNumber));
            startActivity(intent);
            return;
        }
        TelecomManager telecomManager = (TelecomManager)getSystemService(Context.TELECOM_SERVICE);
        if(telecomManager != null) {
            List<PhoneAccountHandle> phoneAccountHandleList = telecomManager.getCallCapablePhoneAccounts();
            d(TAG, "callPhone: " + phoneAccountHandleList);
            d(TAG, "callPhone: " + phoneAccountHandleList.get(1).toString());
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:" + phoneNumber));
            intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandleList.get(1));
            startActivity(intent);
        }
    }

完成代码参考
https://github.com/yunshuipiao/SWBase/blob/Sbranch/app/src/main/java/com/macmini/swensun/swbase/phone/DualSimCallActivity.java

以上情况实现比较粗略,涉及工业代码和机型适配还需细细斟酌。

二叉树的遍历之多种后序遍历

为什么这里仅仅是后序遍历?因此后序遍历稍微要麻烦一点,默认对其他五种遍历已经了解(递归和非递归的前序, 中序遍历,还有层次遍历。
这里有两种情况,

  1. 层次遍历是树的广度优先搜索,借助队列实现。如果要分层输出,那么多加一个变量记录当前层的节点个数。
  2. 其余三种的非递归遍历都是借助栈实现。

递归后序遍历

def post_order_traversal_tree(root):
    if root is None:
        return
    post_order_traversal_tree(root.left_child)
    post_order_traversal_tree(root.right_child)
    print(root.data, end="   ")

下面着重分析一下后序的非递归遍历

非递归遍历(一)

思路:对于任一节点将其入栈。若左右节点都已访问过,则访问该节点并出栈。否则将右子树入栈,左子树入栈, 保证左子树在右子树之前访问。

def post_order_traversal_tree_two(root):
    if root is None:
        return
    node_list = []
    node_list.append(root)
    node_set = set()
    node_set.add(None)
    while len(node_list) != 0:
        temp_node = node_list[-1]
        if temp_node.left_child in node_set and temp_node.right_child in node_set:
            print(temp_node.data, end="   ")
            node_list.pop()
            node_set.add(temp_node)
        else:
            if temp_node.right_child is not None:
                node_list.append(temp_node.right_child)
            if temp_node.left_child is not None:
                node_list.append(temp_node.left_child)
    print()

非递归遍历(二)

思路;对于任一节点将其入栈。然后一直搜索左子树直到空。此时对于栈顶节点,由于还要访问右子树,所以不能出栈。对于该节点的右子树做相同的操作。当右子树访问并出栈后,该节点又出现在栈顶,此时出栈并访问。因此对于每一个节点,都在栈顶出现两次,用一个标记或者集合来判断该节点是第几次出现,做对应的操作。

def post_order_traversal_tree_three(root):
    if root is None:
        return
    node_list = []
    node_set = set()
    node_set.add(None)
    temp_node = root
    while len(node_list) != 0 or temp_node is not None:
        while temp_node is not None:
            node_list.append(temp_node)
            temp_node = temp_node.left_child

        if len(node_list) != 0:
            temp_node = node_list[-1]
            if temp_node not in node_set :  #第一次出现在栈顶,对右子树做相同操作。
                node_set.add(temp_node)
                temp_node = temp_node.right_child
            else:
                print(temp_node.data, end="   ")
                node_list.pop()
                temp_node = None   #最后注意temp_node 出栈后,置为空,防止再次入栈。
    print()

非递归遍历(三)

这一种方式比较讨巧,后序是 左 -- 右 -- 中。
而前序遍历是 中--左--右 ,那么如果能 中--右--左 遍历,倒转就得到了后序遍历的结果,其实也是一种右在先的前序遍历,代码如下:

#根节点入栈。根节点出栈并打印。左子树入栈,右子树入栈。重复上述操作。将打印结果反转即是后序遍历的结果。
def post_order_traversal_tree_four(root):
    if root is None:
        return
    node_list = []
    result = []
    node_list.append(root)
    while len(node_list) != 0:
        temp_node = node_list.pop()
        result.append(temp_node.data)
        if temp_node.left_child is not None:
            node_list.append(temp_node.left_child)
        if temp_node.right_child is not None:
            node_list.append(temp_node.right_child)
    for i in reversed(result):
        print(i, end="   ")
    print()

以上就是我理解的对于二叉树后续遍历的四种方法。
对于其余五种遍历的实现,请查看下面地址,都有简单注释。
github: https://github.com/yunshuipiao/sw-algorithms/tree/master/python

关于这个项目:最基本的算法是每一个计算机从业者必须深刻理解的内容。
因此新建这个项目来学习并用不同语言来实现最基本的算法。
[基本算法:https://github.com/yunshuipiao/sw-algorithms/blob/master/questions.md]
目前语言有python, kotlin, java, c++。

最长递增子序列--动态规划和LCS(最长公共子序列)

最长递增子序列: 动态规划和LCS(最长公共子序列)
子序列和子串的区别:子序列不连续,字串连续。
这个题两种解法

  1. 动态规划
  2. 复制数组并排序,求两数组的最长公共子序列。

下面分别做简单介绍:

动态规划

O(n^2)时间复杂度。想求的array[0, i]的最大递增子序列。则计算array[0, i- 1]中以各元素为最后元素的最长递增序列。与array[i]比较, 因为不连续。

def longest_increasing_subsequence_one(array):
    temp_array = [1] * len(array)
    for index, _ in enumerate(array):
        for i in range(0, index):
            if array[i] < array[index] and temp_array[i] + 1 >= temp_array[index]:
                temp_array[index] = temp_array[i] + 1
    return max(temp_array)

LCS :最长公共子序列

也是动态规划。
若两序列A[i]的元素和B[i]的元素相等,那么最长公共子序列为A[0, i -1], B[0, j - 1]的最长公共子序列的值加1。否则分别是A[0, i- 1], B[0 ,j] 或者A [0, i - 1], B[0, j ]的最长公共子序列的较大值。
其中选择一个二维数组来标记记住A[i], B[j]的最长公共子序列,见代码。

#复制列表并排序,求两列表的最长公共子序列( LCS ): 动态规划
def longest_increasing_subsequence_two(array):
    copy_array = array[:]
    copy_array.sort()
    #中间结果
    temp_array = [[0 for i in range(len(array))] for j in range(len(copy_array))]
    for i in range(1, len(array)):
        for j in range(1, len(copy_array)):
            if array[i] == copy_array[j]:
                temp_array[i][j] = temp_array[i - 1][j - 1] + 1
            else:
                temp_array[i][j] = max(temp_array[i][j - 1],  temp_array[i - 1][j])
    #倒推求出最长公共子序列
    result = []
    i = len(array) - 1
    j = len(copy_array) - 1
    while i > 0 and j > 0:
        if array[i] == copy_array[j]:
            result.append(array[i])
            i -= 1
            j -= 1
        else:
            if temp_array[i][j - 1] >= temp_array[i - 1][j]:
                j -= 1
            else:
                i -= 1
    result.reverse()
    print(result)
    print(len(result))

Rxjava2的listener和响应式解惑

Rxjava2的listener和响应式解惑

标签(空格分隔): 知乎


最近两天一直在被 Rxjava2 的一些用法困扰,比如怎么去写一个 observable 的 listener, 怎么去体现响应式(观察者模式)。网上找了大部分资料,都只涉及怎么简单用订阅,链式发送字符串。
最终在 stackoverflow 找到了灵感,特此记录:


实现一个Obervable的listener

灵感来源:http://stackoverflow.com/questions/25457737/how-to-create-an-observable-from-onclick-event-android

这是一个例子,对大部分监听都适用。使用场景如下:
比如想在 button 点击后异步去获得数据,成功后再主线程更新 UI。实现如下:

//注意listener的实现
Observable.create(new ObservableOnSubscribe<View>() {
            @Override
            public void subscribe(final ObservableEmitter<View> e) throws Exception {
                listener = new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                    //do something
                        if (!e.isDisposed()) {
                            e.onNext(v);
                        }
                    }
                };
            }
        }).subscribe(new Consumer<View>() {
            @Override
            public void accept(@NonNull View view) throws Exception {
                ((Button)view).setText(i++ + "---");
            }
        });
//button监听没有区别
button.setOnClickListener(listener);

这只是一个简单的实现例子,达到同样的效果不难。作为例子,可以实现这样的接口:

    private interface OnGetDataListener {
        void success(String data);
        void errorResult(Error error);
    }
/配合Rxjava2的observer的onNext, onComplete, onError, onSubscribe等去接收,错误时取消订阅等功能。

对大部分事件的响应:比如list的add。

灵感来源:http://stackoverflow.com/questions/28816691/how-can-i-create-an-observer-over-a-dynamic-list-in-rxjava

想在list.add()操作是获得响应,并打印列表。(这只是一个例子)

//首先申明list和一个subject观察者并初始化
private List<Integer> list = new ArrayList<>(0);
private Subject<List<Integer>> subject;

//oncreate()订阅
subject = PublishSubject.create();
subject.subscribe(new Consumer<List<Integer>>() {
        @Override
        public void accept(@NonNull List<Integer> integers) throws Exception {
            Log.d(TAG, "accept: " + integers);
        }
    });
    
//需要的时候发送:list加数据
    private void addInteger(int i) {
        list.add(i++);
        subject.onNext(list);
    }

这是大概流程,可扩展其他接收结果,取消订阅等。

完整代码:https://github.com/yunshuipiao/SWBlog/blob/master/Code/Rxjava.java


踩坑笔记:

今天上午在使用属性动画时,一直在研究重复restart结束的闪动效果。
解决办法:添加线性动画插值器LinearInterpolator即可
默认AccelerateDecelerateInterpolator(先加后减)

2016.11(context, 四大组件, inflate)

as控制台输出中文乱码问题

在当前模块的buide.gradle文件加入:

tasks.withType(org.gradle.api.tasks.compile.JavaCompile) {
    options.encoding = "UTF-8"
}

context的区别

Application mApp = (Application) getApplication();
Log.e(TAG, "getApp: "+mApp);
Context mContext = getApplicationContext();
Log.e(TAG, "onAppContext: "+mContext);
Context mContext2 = mApp.getApplicationContext();
Log.e(TAG, "onAppContext: "+ mContext2);
Log.e(TAG, "onBaseContext: "+ getBaseContext());
Log.e(TAG, "onThis: "+MainActivity.this);
TextView mTextView = (TextView) findViewById(R.id.textview);
Log.e(TAG, "ongetContentDescription(): "+mTextView.getContentDescription());
Log.e(TAG, "onGetContext: " + mTextView.getContext());

//输出结果
com.example.sw.testproject E/MainActivity: getApp: android.app.Application@fd09430
com.example.sw.testproject E/MainActivity: onAppContext: android.app.Application@fd09430
com.example.sw.testproject E/MainActivity: onAppContext: android.app.Application@fd09430
com.example.sw.testproject E/MainActivity: onBaseContext: android.app.ContextImpl@615e8a9
com.example.sw.testproject E/MainActivity: onThis: com.example.sw.testproject.MainActivity@279780b
com.example.sw.testproject E/MainActivity: ongetContentDescription(): null
com.example.sw.testproject E/MainActivity: onGetContext: com.example.sw.testproject.MainActivity@279780b
//说明:getContext()方式是对于一个activity中的view来说,供其调用,返回这个activity。

参考资料:http://stackoverflow.com/questions/10641144/difference-between-getcontext-getapplicationcontext-getbasecontext-and

四大组件的工作过程

activity:展示组件, 接收输入信息和交互,可显示或者隐式Intent启动触发, 前台界面的角色。
Service:计算性组件。有启动和绑定状态,运行在主线程中。
BroadcastRecevier:消息型组件。也叫广播,静态注册和动态注册。
ContentProvider:数据共享型组件。

Intent传递数据的两种方式:

bundle和序列化parcelable。

###判断网络连接状态

    private void checkNetworkConnection() {
      // BEGIN_INCLUDE(connect)
      ConnectivityManager connMgr =
          (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
      NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
      if (activeInfo != null && activeInfo.isConnected()) {
          wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
          mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
          if(wifiConnected) {
              Log.i(TAG, getString(R.string.wifi_connection));
          } else if (mobileConnected){
              Log.i(TAG, getString(R.string.mobile_connection));
          }
      } else {
          Log.i(TAG, getString(R.string.no_wifi_or_mobile));
      }
      // END_INCLUDE(connect)
    }

spannable定义下划线的超链接

String licenseTips = getString(R.string.app_license_tips);
SpannableString ss = new SpannableString(licenseTips);
ss.setSpan(new UnderlineSpan(), 7, licenseTips.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mLicenseTips.setText(ss);

###获取设备宽高

DisplayMetrics dm = getResources().getDisplayMetrics();
int w_screen = dm.widthPixels;
int h_screen = dm.heightPixels;

处理自动问题:

定时器,具体涉及到的工具类,Timer, TimerTask, Handle
现在全部可用Rxjava2代替。

setContentView()

setContentView()方法的内部也是用LayoutInflater来加载布局的,只不过这部分源码是internal的。
layoutInflater的实例之后就可以调用他的inflater方法来加载布局。
layoutInflater.inflate(resouceId, root)
inflate(int resource, ViewGroup parent, boolean attachToRoot): 根据参数名判断用法。

mContentView = LayoutInflater.from(getActivity().inflate(R.layout.fragment_home_contact, null)); //从指定xml加载一个view

自定义一个自动换行的标签页布局

需要重写onMeasure()测量自身宽度,重写onLayout(),算出每个子view的大小,联合屏幕高度进行计算。

//子view的类型和宽高可以自己设定。
public class AutoBreakViewGroup extends ViewGroup {

    private int mScreenWidth;
    private int mHorizontalSpacing;
    private int mVerticalSpacing;

    public AutoBreakViewGroup(Context context) {
        super(context);
        init();
    }

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

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

    private void init() {
        DisplayMetrics dm = getResources().getDisplayMetrics();
        mScreenWidth = dm.widthPixels;
    }

    public void setmHorizontalSpacing(int mHorizontalSpacing) {
        this.mHorizontalSpacing = mHorizontalSpacing;
    }

    public void setmVerticalSpacing(int mVerticalSpacing) {
        this.mVerticalSpacing = mVerticalSpacing;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mTotalHeight = 0;
        int mTotalWidth = 0;

        int mTempHeight = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int measureHeight = childView.getMeasuredHeight();
            int measureWidth = childView.getMeasuredWidth();
            mTempHeight = (measureHeight > mTempHeight) ? measureHeight : mTempHeight;
            if ((measureWidth + mTotalWidth + mHorizontalSpacing) > mScreenWidth) {
                mTotalWidth = 0;
                mTotalHeight += (mTempHeight + mVerticalSpacing);
                mTempHeight = 0;
            }
            childView.layout(mTotalWidth + mHorizontalSpacing, mTotalHeight,
                    measureWidth + mTotalWidth + mHorizontalSpacing, mTotalHeight + measureHeight);
            mTotalWidth += (measureWidth + mHorizontalSpacing);
        }
    }
}

可用google的flexlayout布局替换, 不必重写view。

android网络相关

(http://blog.csdn.net/itachi85/article/details/50982995:网络)

关于android architecture Component的最简单实践

前不久Google IO 2017不仅将kotlin宣布为官方开发语言,还发布了谷歌官方 Android 应用架构库,这个新的架构库旨在帮助我们设计健壮、可测试的和可维护的应用程序。新项目也打算采用这套架构,下面一步步介绍怎么去配置和使用这套架构。(最简单介绍,详细内容看官方文档和其他参考资料)

总览

项目架构

简单说来,该架构由数据驱动, 彻底将UI和data分离,UI层很轻,不涉及任何数据的操作的内容。ViewModel将数据的变化的反映在UI上,本身也不持有数据。官方推荐所有数据持久化。viewmodel通过Repository来管理数据,保存到数据库或者从网络获取。

kotlin的配置

本次打算使用kotlin来进行开发

新建一个项目,完成后在project/build.gradle添加以下内容

//对照添加
buildscript {
    ext.kotlin_version = '1.1.2-5'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

在app/build.gradle的开头添加:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

简单解释:
第二行表示kotlin扩展,可以避免使用类似findviewbyid的很多内容
第三行表示kotlin对注解的处理,感觉对databinding的支持不是很好。
到此为止, kotlin的配置已经完成。

在android studio的插件中安装kotlin后重启。
对Mainactivity.java执行code--convert java file to koltin file。
编译运行成功的话,说明kotlin配置成功

android architecture Component的配置

该架构官方翻译参考:https://juejin.im/post/5937b1d7a22b9d005810b877

简单配置如下
在project/build.gradle中添加:

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

在app/build.gradle中添加

    /// Architecture Components
    compile "android.arch.lifecycle:runtime:$ac_version"
    compile "android.arch.lifecycle:extensions:$ac_version"
    kapt "android.arch.lifecycle:compiler:$ac_version"
    /// Room
    compile "android.arch.persistence.room:runtime:$ac_version"
    kapt "android.arch.persistence.room:compiler:$ac_version"

前面三行是关于lifecycle,livedata, viewmodel的依赖,后面是关于Room的依赖。
后面用例子进行说明。如有问题欢迎讨论。

Room的使用

Room在sqlite之上提供了一个抽象层。
将数据持久化到本地对于应用程序处理大量结构化数据有非常大的好处。最常见的情况是缓存相关数据。这样,当设备无法访问网络时,用户仍然可以在离线状态下浏览内容。然后,在设备重新上线后,任何用户发起的内容变更都会同步到服务器。
其中有三个组件组成:

  • DataBase
  • Entity
  • DAO

下面介绍最简单的使用方法:

项目结构

如上图所示,项目结构对应架构图,另外的base,App是一些基础的ui, 项目的app单例,以后还会添加dagger2来分离模块。
新建一个Story文件,内容如下:

@Entity(tableName = "stories")
class Story {
    @PrimaryKey
    var id = 0
    var data = ""
    var displayData = ""
    var title = ""
    var image = ""
}
//story作为数据库的一张表,可以指定列名,主键

新建StoryDao文件:

@Dao
interface StoryDao {
    @Query("select * from stories")
    fun loadAllStories(): LiveData<List<Story>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertStories(list: List<Story>)
}
//对数据的操作,这里涉及插入和查询(查询的返回值可以是LiveData,list, Rxjava2等)

新建AppDatabase文件:

@Database(entities = arrayOf(Story::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
    companion object {
        val TAG = "sw_story_db"
    }
    abstract fun storyDao(): StoryDao
}
//数据库, 通过dao操作数据。

完成这三个文件,表示Room组件已经可以使用。
新建DataBaseManager文件:

object DatabaseManager {

    lateinit var db: AppDatabase

    fun initDb(context: Context) {
        db = Room.databaseBuilder(context, AppDatabase::class.java, AppDatabase.TAG).build()
    }

    fun insertStories(storys: List<Story>) {
        Flowable.just(storys)
                .observeOn(Schedulers.io())
                .subscribe {
                    db.beginTransaction()
                    try {
                        db.storyDao().insertStories(storys)
                        db.setTransactionSuccessful()
                    } finally {
                        db.endTransaction()
                    }
                }
    }
    fun loadAllStories(): LiveData<List<Story>> {
        return db.storyDao().loadAllStories()
    }

    fun simlutateInsertData() {
        var list = ArrayList<Story>()

        for (i in 1..20) {
            var s = Story()
            s.id = (i)
            s.data = ((i * i).toString() + "--" + i.toString())
            s.displayData = ((i * i).toString() + "--" + i.toString())
            s.title = ((i * i).toString() + "--" + i.toString())
            s.image = ((i * i).toString() + "--" + i.toString())
            list.add(s)
        }
        insertStories(list)
    }
}
//该单例对数据库封装了一层,方便处理db的各种操作(db初始化可使用依赖注入)
数据库插入不能在主线程,涉及事务。

到此为止,该框架的数据库部分已经可以使用。
下面作为例子说明怎么存数据。

//继承Appication,初始化并模拟插入数据
class ZhiJokeApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        DatabaseManager.initDb(this)
        DatabaseManager.simlutateInsertData()
    }
}

编译项目运行,此时真机上应该保存有插入的数据。

####怎么查看
用一台已经root后的测试机,安装root文件管理器后,在根目录的 data/data/包名/database, 就能看到数据库新建并插入的数据了。

DatabaseManage.loadAllStories()即可取出数据。

未玩待续

完整代码:github

在服务器的docker中运行scrapy

前面的文章介绍了怎么用scrapy去完成一个爬虫,涉及动态抓取和登录等操作。这篇文章简单介绍怎么让爬虫运行在服务器的docker里。

步骤

  1. 利用sshfs将服务器磁盘挂载到本地,实现本地开发,省去同步代码等步骤。
  2. docker通过pull或者Dockerfile拉取镜像。
  3. 通过docker-compose配置镜像,在启动的镜像里启动爬虫。

详细步骤:

首先安装sshfs工具,可以参考网上教程。
接着切换到个人目录,新建 aliyun文件夹。

localhost:~ swensun$ cd ~
localhost:~ swensun$ mkdir aliyun
localhost:~ swensun$ sshfs 120.78.202.210:/  aliyun/

之后输入密码,打开aliyun目录,即可看到的服务器上的目录挂载到了本地,可以本地编辑,保存。

  1. 切换到个人目录,新建docker文件夹,再新建爬虫的spider文件夹。
    路径如下:
    /Users/swensun/aliyun/home/swensun/App/docker/spider
  • 下载Dockerfile(也可以使用pull)下载docker镜像。

在该目录下新建Dockerfile文件,最简单内容如下,

FROM python:latest

RUN apt-get update \
	&& apt-get install -y vim \
	&& rm -rf /var/lib/apt/lists/*

这里执行了最简单的操作,下载python镜像,并执行RUN后面的命令。

ssh连接到服务器, 切到相应目录,
执行sudo docker build -t pythonspider . 命令, 下载生成镜像, 输出如下:
image.png
执行sudo docker images命令,可以看到已经生成了pythonspider镜像。
image.png

  1. 将本地写的爬虫复制到Dockerfile目录,并新建docker-compose.yml文件。
    文件目录如下:
    image.png

docker-compose.yml文件内容:

version: '2'
services:
  spider:
    image: pythonspider      
    volumes:
      - ./zhihu:/spider
    tty: true

简单解释:services可启动多个服务,比如数据库,nignx和执行程序配合生成一个容器。该服务叫spider,利用前面下载的pythonspider镜像,将./zhihu数据卷同步到docker中。tty保证创建容器后保持后台运行,以免创建后关闭。
执行如下命令:
image.png
image.png
如上,可以看到创建的容器正在运行。
下面进入容器运行爬虫
image.png

可以看到数据卷spider已经在根目录。进入根目录,执行爬虫(需要安装python需要的包,也可以在前面的dockerfile中安装)。
image.png
可以看到和本地输出了一样的结果,说明docker中运行scrapy成功。

关于Dockerfile和docker-compose的其他命令及其高级用法,我也不是很懂,后面慢慢研究。

总结:
本文介绍了sshfs工具的使用,以及docker的安装与部署。
(Dockerfile和docker-compose)。

weixin:youquwen1226
github:https://github.com/yunshuipiao

欢迎来信一起探讨。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.