Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

《Android系统源代码情景分析》连载回忆录:灵感之源

$
0
0

       上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView。学到了很多东西,不过也挺累的,平均不到两个星期一篇文章。本来想休息一段时间后,再继续分析Chromium使用的JS引擎V8。不过某天晚上,躺在床上睡不着,鬼使神差想着去创建一个个人站点,用来连载《Android系统源代码情景分析》一书的内容。

       事情是这样的,躺在床上睡不着,就去申请了一个域名,0xcc0xcd.com。域名申请到了,总不能不用吧。用来做什么呢?想起我写的那本书《Android系统源代码情景分析》,从2012年10月出版至今,也有四年多的时间了,得到了大家的厚受。不过网络上也逐渐的出现了一些盗版PDF。不用说,质量肯定很差。干脆我把这本书的内容在我的个人站点上放出来吧。后面征得了出版社的同意,就着手开始干了。

       网站名称为“进击的程序员”,主要是为了配合0xcc0xcd.com这个域名。从Windows时代过来的老司机可能一眼就能看出这个域名是什么意思。看不懂的,如果大家有兴趣,后面我也可以详细说说,怀念一下逝去的青春。

       从开始有想法,到把网站建好,以及将书前三章(准备知识、硬件抽象层、智能指针)的内容放上去,花了不到一个月的时间。在这不到一个月的时间里,学习到了挺多东西:申请域名、云服务器、域名解析、域名邮箱、网站备案以及开发网站等等。因为我一直都是做客户端开发,刚毕业几年做的是Windows客户端,后面做的是Android端,没有做过网站相关的开发,包含前端和后端,所以学习过程还是有些小波折。不过总体上来说还是比较顺利的。这也跟网站的技术选型有关吧。

       现在不是提倡做全栈工程师吗?这个建站过程也算是小小地实践了一把。怕时间久了会忘记一些关键细节和踩过的坑,所以就计划把建站连载书的过程记录下来。也希望能够帮助到有兴趣做全栈工程师的同学们。

       网站使用的是LNMP架构,如下图1所示:


图1 进击的程序员网站架构

       网站运行在云服务器上,系统装的是Ubuntu 14.04,除了Nginx、PHP和MySQL,还搭了一个GIT仓库,用来管理网站源码。这个GIT仓库除了用来管理网站源码,还用来将源码分布到网站中去。

       具体是这样的,在本地用自己的电脑开发网站(其实就是用vim编辑网页和PHP)。测试没有问题之后,就用git push命令将源码上传到GIT仓库。然后再登录到云服务器上,在网站根目录用git pull命令从GIT仓库中获得最新网站源码。

       此外,在本地还搭建了一个管理后台。这个管理后台就是用来给管理员管理网站的。主要就是操作一下数据库,例如查看数据、插入数据、更新数据等等。正规的网站会专门提供一些页面供管理员操作。鉴于这个网站不是很正规,管理员又是一个技术控,于是就直接使用Python脚本来实现这个管理后台了,想要什么功能就直接写个脚本。

      Oracle提供了一个Python版的MySQL数据库驱动库MySQL Connector/Python,通过它很容易用Python脚本操作MySQL中的数据。这样一个简单的管理后台就搭建起来了。

      整个网站的架构非常简单,可以非常快上手,同时它又五脏俱全。网站的前端主要用Ajax、jQuery开发,后端没有用什么高大尚的框架,基本上是徒手写的PHP。主要是考虑这个网站要做的事情很简单,就是连载《Android系统源代码情景分析》的内容,基本功能就是浏览和评论。所以就以最简单最快的方式实现。

      为了让大家利用碎片时间更好地阅读书的内容,网站在提供PC版的同时,也提供了移动版。移动版和PC版的功能是一样的,只是它们的页面表现形式不一样。所以网站在设计之初,就考虑了模块化和代码复用,用最小的成本获得同时实现PC端和移动端的功能。

      不知道为什么,说起PHP, 总是会想起“PHP是最好的语言”这句话。从这一个月的经历看,PHP是不是最好的语言不知道,但是用来建网站,PHP的确是最好的语言。用PHP和JS开发网站,效率比用Java/OC开发App,高多了。不过,网站的体验不如App。所以移动开发目前还是王道。

       接下来,我会用一个系列的文章分享整个建站过程,包括:

       1. 域名、云服务器、域名解析、网站备案、域名邮箱、CA证书申请

       2. LNMP开发环境搭建,包括如何配置SSL加密的HTTPS站点

       3. 支持SSH访问的GIT仓库搭建

       4. 网站基本功能开发,PC版和移动版代码复用

       5. 基于MySQL Connector/Python的管理后台开发

       欢迎大家关注!想在线阅读《Android系统源代码情景分析》一书的,点击进击的程序员进入!

作者:Luoshengyang 发表于2017/1/10 22:42:11 原文链接
阅读:40484 评论:22 查看评论

NVIDIA Jetson TX1 系列开发教程之十二:libcurl、RapidJSON安装

$
0
0

NVIDIA Jetson TX1 系列开发教程之十二:libcurl、RapidJSON安装



前言

  由于项目需要,WordZzzz最近尝试在TX1上解析服务器端传回json信息。先用Python3进行了测试,Python作为一门脚本语言,用来做测试简直棒极了。但是,我们最终还是要把代码移植到linux C上来进行实现。

  虽说大家能看到这篇文章一般都是想直接解决问题的,但是我还是在此啰嗦几句,做个小小的科普。

  JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

JSON建构于两种结构:

  • “名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。
  • 值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。
    这些都是常见的数据结构。事实上大部分现代计算机语言都以某种形式支持它们。这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。
  • 再详细点的话,就请进入传送门吧:http://www.json.org/json-zh.html

  用c在linux下实现json解析,需要两个工具,一个是可以实现http通信的工具,这里WordZzzz用的是libcurl(当然也可以用curl或者wget来模拟http的post和get);另一个是可以解析json信息的工具,这里WordZzzz用的是腾讯开源的RapidJSON(当然大家也可以尝试json-c等)。

libcurl

简介

  • cURL是一个利用URL语法在命令行下工作的文件传输工具,1997年首次发行。它支持文件上传和下载,所以是综合传输工具,但按传统,习惯称cURL为下载工具。cURL还包含了用于程序开发的libcurl。
  • cURL支持的通信协议有FTP、FTPS、HTTP、HTTPS、TFTP、SFTP、Gopher、SCP、Telnet、DICT、FILE、LDAP、LDAPS、IMAP、POP3、SMTP和RTSP。
  • libcurl支持的平台有Solaris、NetBSD、FreeBSD、OpenBSD、Darwin、HP-UX、IRIX、AIX、Tru64、Linux、UnixWare、HURD、Windows、Symbian、Amiga、OS/2、BeOS、Mac OS X、Ultrix、QNX、BlackBerry Tablet OS、OpenVMS、RISC OS、Novell NetWare、DOS等。
  • 想要了解更多信息,官网传送门:https://curl.haxx.se/

安装

1.下载libcurl源码:

$ git clone https://github.com/curl/curl.git

2.配置:

$ ./buidconf
$ ./configure --enable-debug

  第一步用于生成configure配置文件,第二步进行配置。可以./configure –help查看其他可选参数。我是默认安装openssl的,所以没有出现找不到openssl库的问题。如果遇到了就装一个,选择默认安装省事,自己指定安装目录比较麻烦。具体查看工程目录下的README。

3.编译&安装:

  默认库文件安装在/usr/local/lib 头文件安装在/usr/local/include —>安装要root权限

$ make
$ sudo make install

4.测试代码:
  接下来写个测试代码来使用libcurl库(此测试代码下载指定URL的页面)。
  测试代码摘自网络,如下:

// 采用CURLOPT_WRITEFUNCTION 实现网页下载保存功能  

#include <stdio.h>;  
#include <stdlib.h>;  
#include <unistd.h>;  

#include <curl/curl.h>;  
#include <curl/types.h>;  
#include <curl/easy.h>;  

FILE *fp;  //定义FILE类型指针  
//这个函数是为了符合CURLOPT_WRITEFUNCTION而构造的  
//完成数据保存功能  
size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream)    
{  
    int written = fwrite(ptr, size, nmemb, (FILE *)fp);  
    return written;  
}  

int main(int argc, char *argv[])  
{  
    CURL *curl;  
    if (argc != 3)  
    {  
        fprintf(stderr, "usage: %s url filename\n", argv[0]);  
        exit(-1);  
    }  
    curl_global_init(CURL_GLOBAL_ALL);    
    curl = curl_easy_init();  
    curl_easy_setopt(curl, CURLOPT_URL, argv[1]);    

    if((fp = fopen(argv[2],"w")) == NULL)  
    {  
        curl_easy_cleanup(curl);  
        exit(1);  
    }  
  //CURLOPT_WRITEFUNCTION 将后继的动作交给write_data函数处理  
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);    
    curl_easy_perform(curl);  
    curl_easy_cleanup(curl);  
    exit(0);  
}  

5.查看库链接信息:

$ pkg-config --cflags --libs libcurl
-I/usr/local/include -L/usr/local/lib -lcurl

  返回信息如上,其中-I后面的是头文件路径,-L后面的是链接库路径,链接库名称为curl。

6.编译测试代码:

  可以直接用nsight进行配置(详情参考我之前的nsight基础教程:http://blog.csdn.net/u011475210/article/details/72853170、nsight进阶教程:http://blog.csdn.net/u011475210/article/details/72860277),也可以直接手写gcc命令进行编译。

$ gcc -L/usr/local/lib -o test test.c -lcurl 

7.编译完成,运行程序:

$ ./test

  程序会将百度首页下载下来,存为index.html。

RapidJSON

简介

  RapidJSON 是一个 C++ 的 JSON 解析器及生成器。它的灵感来自 RapidXml。

  • RapidJSON 小而全。它同时支持 SAX 和 DOM 风格的 API。SAX 解析器只有约 500 行代码。
  • RapidJSON 快。它的性能可与 strlen() 相比。可支持 SSE2/SSE4.2 加速。
  • RapidJSON 独立。它不依赖于 BOOST 等外部库。它甚至不依赖于 STL。
  • RapidJSON 对内存友好。在大部分 32/64 位机器上,每个 JSON 值只占 16 字节(除字符串外)。它预设使用一个快速的内存分配器,令分析器可以紧凑地分配内存。
  • RapidJSON 对 Unicode 友好。它支持 UTF-8、UTF-16、UTF-32 (大端序/小端序),并内部支持这些编码的检测、校验及转码。例如,RapidJSON 可以在分析一个 UTF-8 文件至 DOM 时,把当中的 JSON 字符串转码至 UTF-16。它也支持代理对(surrogate pair)及 “\u0000”(空字符)。
  • 想要了解更多信息,官网传送门:http://rapidjson.org/zh-cn/

安装

1.cmake:
  采用cmake方式进行安装,所以cmake命令得有。

$ sudo apt-get install cmake

2.下载源码:

$ git clone https://github.com/Tencent/rapidjson.git

3.编译&安装:

$ cd rapidjson/
$ mkdir build
$ cd build
$ cmake ..
$ sudo make install

4.查看库链接信息:

$ pkg-config --cflags --libs RapidJSON
-I/usr/local/include

  可以发现只有头文件信息,我们只需要调用头文件而不需要链接库,所以很方便。

5.官方例程:

// rapidjson/example/simpledom/simpledom.cpp`
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#include <iostream>
using namespace rapidjson;
int main() {
    // 1. 把 JSON 解析至 DOM。
    const char* json = "{\"project\":\"rapidjson\",\"stars\":10}";
    Document d;
    d.Parse(json);
    // 2. 利用 DOM 作出修改。
    Value& s = d["stars"];
    s.SetInt(s.GetInt() + 1);
    // 3. 把 DOM 转换(stringify)成 JSON。
    StringBuffer buffer;
    Writer<StringBuffer> writer(buffer);
    d.Accept(writer);
    // Output {"project":"rapidjson","stars":11}
    std::cout << buffer.GetString() << std::endl;
    return 0;
}

  注意此例子并没有处理潜在错误。

6.编译例程:

$ g++ -o simpledom simpledom.cpp

7.运行:

$ ./simpledom

图解过程:

输出结果:

{"project":"rapidjson","stars":11}

系列教程持续发布中,欢迎订阅、关注、收藏、评论、点赞哦~~( ̄▽ ̄~)~

完的汪(∪。∪)。。。zzz

作者:u011475210 发表于2017/9/24 17:20:09 原文链接
阅读:1011 评论:0 查看评论

react native学习笔记10——react-navigation的嵌套使用

$
0
0

前面介绍了react-navigation的StackNavigator、TabNavigator、DrawerNavigator 的基本使用方法,在实际项目中通常需要嵌套这几种导航方式。下面介绍这几种导航方式的嵌套使用方法。

1.StackNavigator与TabNavigator的嵌套

使用StackNavigator与TabNavigator嵌套的App有很多,例如淘宝、京东等,下部选项卡导航,页面中的按钮点击会跳转到二级页面,有的甚至有三级四级页面。
首先用TabNavigator构建出选项卡导航,新建tab的首页MainScreen.js。详细的分解步骤可以参考上一篇的react-navigation的基本使用中TabNavigator的使用介绍。
MainScreen.js

import React from 'react';
import {
    AppRegistry,
    Text,
    View,
    Button,
    StyleSheet,
    Image,
} from 'react-native';
import SecondScreen from "./SecondScreen";
import { TabNavigator } from "react-navigation";

class TabMainScreen extends React.Component {
    static navigationOptions = {
        tabBarLabel: 'Home',
        // Note: By default the icon is only shown on iOS. Search the showIcon option below.
        tabBarIcon: ({ tintColor }) => (
            <Image
                source={require('../../images/color_for_danmu_normal.png')}
                style={[styles.icon, {tintColor: tintColor}]}
            />
        ),
    };

    render() {
        return (
            <Button
                onPress={() => this.props.navigation.navigate('Second')}
                title="Go to Second"
            />
        );
    }
}
const styles = StyleSheet.create({
    icon: {
        width: 26,
        height: 26,
    },
});

const TabMainScreenNavigator = TabNavigator(
    {
        Main: { screen: TabMainScreen },
        Second: { screen: SecondScreen },
    },
    {
        tabBarPosition: 'bottom',
        animationEnabled: true,
        tabBarOptions: {
            activeTintColor: '#e91e63',
            showIcon:'true'
        },
    }
);

export default TabMainScreenNavigator;

然后是第二个tab页SecondScreen.js

import React from 'react';
import {
    AppRegistry,
    Text,
    View,
    Button,
    StyleSheet,
    Image,
} from 'react-native';
import { StackNavigator } from 'react-navigation';
export default class SecondScreen extends React.Component {
    static navigationOptions = {
        tabBarLabel: 'Second',
        tabBarIcon: ({ tintColor }) => (
            <Image
                source={require('../../images/face_unpress.png')}
                style={[styles.icon, {tintColor: tintColor}]}
            />
        ),
    };

    render() {
        return (

            <Button
                onPress={() => this.props.navigation.goBack()}
                title="Go back home"
            />
        );
    }
}

const styles = StyleSheet.create({
    icon: {
        width: 26,
        height: 26,
    },
});

这样有如下效果

嵌套StackNavigator
在同一路径下新建MyStackNavigation.js。

import React, { Component } from 'react';
import {
    AppRegistry,
} from 'react-native';
import { StackNavigator  } from 'react-navigation';
import ThirdScreen from "./ThirdScreen";
import TabMainScreen from "./TabMainScreen";

const MyStackNavigation  = StackNavigator({
    Main: { screen: TabMainScreen },
    Third: { screen: ThirdScreen },
});
export default MyStackNavigation;

修改之前的TabMainScreen.js
navigationOptions中加上headerTitle导航栏的标题Main

static navigationOptions = {
        tabBarLabel: 'Home',
        headerTitle:'Main',
        tabBarIcon: ({ tintColor }) => (
            <Image
                source={require('../../images/color_for_danmu_normal.png')}
                style={[styles.icon, {tintColor: tintColor}]}
            />
        ),
    };

主界面中添加一个跳转到第三个页面的按钮。render函数中返回View改为:

render() {
        return (
            <View style={{ backgroundColor: "#fff", flex: 1, padding: 20}}>
                <Button
                    onPress={() => this.props.navigation.navigate('Third',{ user: 'Lucy' })}
                    title="Go to Third"
                />
                <View style={{ backgroundColor: "#fff", height: 20}}/>
                <Button
                    onPress={() => this.props.navigation.navigate('Second')}
                    title="Go to Second"
                />
            </View>
        );
    }

再新建第三个页面ThirdScreen,在同一目录下新建ThirdScreen.js。

import React, {Component} from "react";
import {StyleSheet,View, Text } from "react-native";

export default class ThirdScreen extends React.Component {
    static navigationOptions = ({ navigation }) => {
        const {state, setParams} = navigation;
        const isInfo = state.params.mode === 'info';
        const {user} = state.params;
        return {
            title: isInfo ? `${user}'s Contact Info` : `Chat with ${state.params.user}`,
        };
    };
    render() {
        return <Text>I am the third Screen</Text>
    }
}

这样就完成了StackNavigator与TabNavigator的嵌套,在要引用的父组件中引用MyStackNavigation即可,例如我的Demo中,入口是App.js,在App.js引用:

import React, { Component } from 'react';
import {
    AppRegistry,
} from 'react-native';
import MyStackNavigation from "./src/05_navigation/nestingnavigators/MyStackNavigation";
export default class ExerciseProject extends Component {
    render() {
        return (
            <MyStackNavigation />
        );
    }
}
AppRegistry.registerComponent('ExerciseProject', () => ExerciseProject);

效果如下:

源码在这里

2.DrawerNavigator、StackNavigator与TabNavigator的三层嵌套

它们的嵌套关系为:DrawerNavigator>StackNavigator>TabNavigator。
之所以把DrawerNavigator放在最外层是由于如果DrawerNavigator外层还有别的Navigator,则侧滑栏会在父组件的下面,如果父组件是底部TabNavigator,则出现在标签栏上面。那样效果比较怪,与原生开发出的效果不一致。

在上面代码的基础上,新建MyDrawerNavigation.js,设置抽屉导航的首页Home为MyStackNavigation,第二个菜单栏DrawerSecond为DrawerSecondScreen。

import React from 'react';
import {
    AppRegistry,
    Text,
    View,
    Button,
    StyleSheet,
    Image,
} from 'react-native';
import TabMainScreen from "./TabMainScreen";
import DrawerSecondScreen from "./DrawerSecondScreen";
import MyStackNavigation from "./MyStackNavigation";
import { DrawerNavigator } from "react-navigation";

const MyDrawerNavigation = DrawerNavigator(
    {
        Home: {
            screen: MyStackNavigation,
            navigationOptions: {
                drawerLabel: 'Home',
                drawerIcon: ({ tintColor }) => (
                    <Image
                        source={require('../../images/color_for_danmu_normal.png')}
                        style={{tintColor: tintColor, width: 24, height: 24,}}
                    />
                ),
            },
        },
        DrawerSecond: {
            screen: DrawerSecondScreen,
            navigationOptions: {
                drawerLabel: 'DrawerSecond',
                drawerIcon: ({ tintColor }) => (
                    <Image
                        source={require('../../images/face_unpress.png')}
                        style={ {tintColor: tintColor, width: 24, height: 24,}}
                    />
                ),
            },
        },
    }
);

export default MyDrawerNavigation;

新建侧滑栏导航栏的第二页DrawerSecondScreen.js

import React from 'react';
import {
    Button,
    View,
} from 'react-native';
import { StackNavigator } from 'react-navigation';
export default class DrawerSecondScreen extends React.Component {
    render() {
        return (
            <View style={{ backgroundColor: "#fff", flex: 1, padding: 20}}>
                <Button
                    onPress={() => this.props.navigation.goBack()}
                    title="Go back home"
                />
        </View>
        );
    }
}

效果如下:

但如此需要从左侧拉出侧滑栏的操作多少有些不便,通常侧滑栏导航在标题栏左侧会有一个按钮,点击可以打开侧滑栏。
打开侧滑栏的关键语句是

this.props.navigation.navigate('DrawerOpen');

修改MyStackNavigation.js,在StackNavigator添加navigationOptions的设置,加上标题栏左侧按钮headerLeft,并顺便将标题居中。MyStackNavigation.js修改后的代码为:

import React, { Component } from 'react';
import {
    AppRegistry,
    Button,
} from 'react-native';
import { StackNavigator  } from 'react-navigation';
import ThirdScreen from "./ThirdScreen";
import TabMainScreen from "./TabMainScreen";

const MyStackNavigation  = StackNavigator({
    Main: {
        screen: TabMainScreen,
        navigationOptions:({ navigation }) => ({
            headerTitleStyle:{
                alignSelf:'center',
            },
            headerLeft: (
                <Button
                    title='Menu'
                    onPress={() => navigation.navigate('DrawerOpen')}
                />
            ),
        }),
    },
    Third: {
        screen: ThirdScreen,
        navigationOptions:({ navigation }) => ({
            headerTitleStyle:{
                alignSelf:'center',
            },
            headerLeft: (
                <Button
                    title='Menu'
                    onPress={() => navigation.navigate('DrawerOpen')}
                />
            ),
        }),
    },

});
export default MyStackNavigation;

这样通过点击标题栏左侧的Menu按钮,即可打开侧滑栏了:

示例的源码在这里

作者:teagreen_red 发表于2017/9/24 17:42:23 原文链接
阅读:219 评论:0 查看评论

一步步打造自己的通用上拉加载布局

$
0
0

背景

下拉刷新是App交互中非常常见的场景,而与其对应的上拉加载,在很多场景中也已经是用户意识中理所当然的一种交互了。

在很久之前的项目开发中,就已经有上拉加载的这个需求。但是那时苦于没有找到一个合适的上拉加载的库,而项目迭代又紧,那时自己实现恐时间上来不及或者引入其他bug,就暂时用了秋百万的cube-sdk中的点击加载。
在今年该项目的又一次迭代开发中,由于使用到了RecyclerView,而对应的RecyclerView.Adapter又无法使用cube-sdk中的adapter,因此用不了其点击加载,考虑到自己这两年所积累的相关知识及对上拉加载的思考应已足够,就花了些时间,实现了一个相对简单的上拉加载布局。

思考

我对上拉加载的思考受影响于两年前读过的秋百万的一篇文章《我眼中的下拉刷新》。但是上拉加载与下拉刷新的差异,不止是拉的方向不同,它们所拉出来的Header或Footer在加载完成后的消失方式也会不同,这就导致了在实现层面上会有些区别。

先说下拉刷新,通常是先让一个HeaderView位于ContentView外部而不显示出来,然后在下拉的时候让它与ContentView(或只有HeaderView)跟着移动下来,然后到一定距离触发刷新,HeaderView回滚到顶部停留,等刷新完成再慢慢滑动出去。

而上拉加载,通常的场景是用于AbsListView或RecyclerView。它与下拉刷新的最大不同是,所加载出来的内容会插入到当前所显示的AbsListView或ReyclcerView中,并显示在原来最后显示的内容与FooterView之间。
以RecyclerView举例,当我们在上拉加载更多的布局里放一个RecyclerView与一个FooterView,并把FooterView设置在布局底部范围之后,然后让它随着RecyclerView一起上拉,并显示出来,这点并没有问题。这时的界面如下图:

这里写图片描述
这时我们思考一个问题:当数据加载完成,更新到RecyclerView中时,界面应该如何处理?
通常而言,这时候应该是新加载的数据从FooterView的位置开始显示,而FooterView消失。但我们让FooterView消失(移出显示范围之外),而让RecyclerView移回来,所加载的新内容就会在屏幕外面,需要用户再去手动滑动上来才能看到。这种体验就很不好了。
因此我个人觉得,这个FooterView不应该由我们的上拉加载的布局去控制,而是交由具体场景去实现,在上拉加载的布局当中,应只做ContentView的位移,以及相关的界面及功能接口的回调。而除此外我们需要做的,是提供一些接口,来实现上拉UI需求上的灵活性及可定制化。

基本接口

为了让UI上有更大的灵活性,我们需要对上拉加载的UI变化进行一些解耦。参考秋百万的下拉刷新的库,又考虑到目前实现比较简单的上拉加载,所以我先定义了以下两个接口:
一是上拉加载的UI回调接口,它应该至少有三个状态变化的回调:可以上拉,已经触发加载回调,上拉完成。除此之外,为配合实现一些更好的提示或动画,它至少需要提供两个值:能够触发加载的位移量,以及当前的位移量。当然,多一些其他参数,比如当前的位移方向、速度等的话,可以实现更多的效果,不过这里只是先完成基本功能,所以实现上就先简单点。根据所需要的这些回调,LoadMoreUIHandler接口定义如下:

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.hiloadmore;

/**
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-03 0.1
 */
public interface LoadMoreUIHandler {
    void onPrepare();

    void onBegin();

    void onComplete(boolean hasMore);

    void onPositionChange(int offsetY, int offsetToLoadMore);
}

第二个接口是触发加载的回调接口,只有一个方法,如下:

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.hiloadmore;

/**
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-02 0.1
 */
public interface LoadMoreHandler {

    void onLoadMore();
}

具体实现

我们首先来实现上拉。注意,由于API 14已能适配目前市场上所有Android设备,所以这里像判断是否可以上下拉动或对View进行位移操作,会直接使用到一些API 14以上才有的接口。

首先布局直接继承自FrameLayout。其次,上拉过程需要知道当前的状态,能触发拉动的位移量,当前位移量,是否可以上拉等,所以定义变量,构造方法及一些基本的getter和setter方法如下:

public class LoadMoreLayout extends FrameLayout {

    private static final byte STATUS_INIT = 0;
    private static final byte STATUS_PREPARE = 1;
    private static final byte STATUS_LOADING = 2;
    private static final byte STATUS_COMPLETE = 3;

    private byte mStatus = STATUS_INIT; //上拉状态

    View mContent;
    private int mCurrentOffsetY; //当前位移量
    private int mOffsetYToLoadMore = 200; // 触发加载至少需要的位移量
    private float mResistance = (float) Math.PI; // View实际的位移量=手指拖动的量/它

    private float mDownY; //手指按下时的Y坐标

    private int mDragSlop; //判断触发拖动操作的阙值

    private boolean mHasMore; // 是否可以加载更多

    private LoadMoreHandler mLoadMoreHandler;
    private LoadMoreUIHandler mLoadMoreUIHandler;

    public LoadMoreLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDragSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    }

    public void setHasMore(boolean hasMore) {
        mHasMore = hasMore;
    }

    protected boolean hasMore() {
        return mHasMore;
    }

    public void setOffsetYToLoadMore(int offsetYToLoadMore) {
        mOffsetYToLoadMore = offsetYToLoadMore;
    }

    public void setResistance(float resistance) {
        mResistance = resistance;
    }

    public void setLoadMoreHandler(LoadMoreHandler loadMoreHandler) {
        mLoadMoreHandler = loadMoreHandler;
    }

    public void setLoadMoreUIHandler(LoadMoreUIHandler loadMoreUIHandler) {
        mLoadMoreUIHandler = loadMoreUIHandler;
    }
    //...
}

接下来,我们需要找到我们的ContentView,这里提供两种方式:一是获取布局里的第一个子View,二是提供一个设置ContentView的方法:

    public void setContentView(View view) {
        mContent = view;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        final int childCount = getChildCount();
        if (childCount < 1) {
            throw new IllegalStateException("LoadMoreLayout needs at least one child");
        }
        if (mContent == null) {
            mContent = getChildAt(0);
            mContent.bringToFront();
        }
    }

接下来重写onLayout方法,确保在整个过程当中不会因layout操作导致内容位移位置不正确。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int offsetY = mCurrentOffsetY;
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();

        if (mContent != null) {
            MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
            final int left = paddingLeft + lp.leftMargin;
            final int top = paddingTop + lp.topMargin + offsetY;
            final int right = left + mContent.getMeasuredWidth();
            final int bottom = top + mContent.getMeasuredHeight();
            mContent.layout(left, top, right, bottom);
        }
    }

接下来就是对手指的事件处理了,这也是完成上拉加载的关键之一。

首先是事件拦截,我们要先判断是否可以进行上拉或由LoadMoreLayout下拉,如果可以,则拦截事件,不让事件再往下传递,所以这里重写onInterceptTouchEvent(MotionEvent ev)方法:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnabled() || mContent == null || !mHasMore) {
            return super.onInterceptTouchEvent(ev);
        }

        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = ev.getY();
                // TODO 停止往回滑动
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetY = (int) (ev.getY() - mDownY);//当前拖动距离
                if (Math.abs(offsetY) < mDragSlop) {
                    //小于可判定为拖动的阙值则不处理
                    break;
                }
                boolean moveUp = offsetY < 0;
                boolean canMoveDown = mCurrentOffsetY < 0;
                if (moveUp && mContent.canScrollVertically(1)) {//如果子View可以继续往下滑动,则不拦截
                    break;
                }
                if (moveUp || canMoveDown) {
                    intercept = true;
                }
                break;
        }

        return intercept || super.onInterceptTouchEvent(ev);
    }

然后重写onTouchEvent(MotionEvent ev)方法,进行上拉加载的逻辑,以及移动ContentView的位置。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float offsetY = event.getY() - mDownY;
                if (mStatus != STATUS_LOADING && mStatus != STATUS_PREPARE) {
                    mStatus = STATUS_PREPARE;
                    mLoadMoreUIHandler.onPrepare();
                }
                movePos((int) (offsetY / mResistance));
                if (mStatus == STATUS_PREPARE) {
                    mLoadMoreUIHandler.onPositionChange(mCurrentOffsetY, mOffsetYToLoadMore);
                }
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                onRelease();
                return true;
        }
        return super.onTouchEvent(event);
    }

movePos(int)实现对ContentView的位移,如下:

    private void movePos(int offsetY) {
        if (offsetY > 0 && mCurrentOffsetY == 0) {
            return;
        }
        if (offsetY > 0) {
            offsetY = 0;
        }
        mContent.setTranslationY(offsetY);
        mCurrentOffsetY = offsetY;
    }

onRelease()是手放开后判断是否触发加载,以及让ContentView归位的操作:

    private void onRelease() {
        performLoadMore();
        // TODO 让ContentView归位
    }

    private void performLoadMore() {
        if (mStatus != STATUS_PREPARE) {
            return;
        }
        if (Math.abs(mCurrentOffsetY) >= mOffsetYToLoadMore) {
            mStatus = STATUS_LOADING;
            mLoadMoreHandler.onLoadMore();
            mLoadMoreUIHandler.onBegin();
        } else {
            mLoadMoreUIHandler.onPrepare();
        }
    }

以上完成了上拉时对ContentView的位移,以及回调加载方法。但这只是完成了从最初的状态到开始的状态,我们还需要知道加载完成,这样才能让状态重置,以及知道是否还可以继续加载。所以还需要有如下方法:

    public void loadMoreComplete(boolean hasMore) {
        mHasMore = hasMore;
        mLoadMoreUIHandler.onComplete(hasMore);
        mStatus = STATUS_COMPLETE;
    }

除此之外,我们还增加一个方法,用于外界触发它开始加载,可用于自动加载的实现。

    public void triggerToLoadMore() {
        if (!mHasMore || mStatus == STATUS_LOADING) {
            return;
        }
        mStatus = STATUS_LOADING;
        mLoadMoreHandler.onLoadMore();
        mLoadMoreUIHandler.onBegin();
    }

到这里,我们已经完成了从初始状态到上拉到加载到完成的整个过程。但是如果你够细心会发现,目前为止并没有提到如何让ContentView回去,并且上面的代码中有两处TODO的标记。因此如果一直上拉,最终是会把ContentView给拉出外面的。所以,我们接下来还要实现让ContentView回来的代码。

我们知道,让一个View产生位移有多种方式,比如设置它的margin,设置父布局的padding,调用它的layout方法,或者是如上面我们的实现中使用setTranslationY(float) 方法。而让View滑动回去,由于此过程当中并不需要跟着手指来移动,所以也会有几种选择。
首先,既然前面我们是使用setTranslationY(float)来设置它的位置,那么最终肯定也是需要调用这个方法来恢复原位的。而在中间的过程当中,可供选择的处理方式至少有:

  • 先调用该方法直接设置回去,然后播放一个位移动画。简单粗暴。
  • 使用Scroller计算每次的位移量,然后调用这个ContentView的setTranslationY(float)方法设置它的位置让它慢慢回去。

由于第二种方式它所处的位置与我们所记录的位移量是对应上的,并且在回滚过程当中当我们的手指按下去,是可以让它停住的,相对而言更为真实,所以这里选用第二种方式。
参考了秋百万的下拉刷新的库,这里定义了一个内部类,代码如下:

    class ScrollChecker implements Runnable {
        private static final int MOVE_DELAY = 12;

        private final Scroller mScroller;

        private int mStart;
        private boolean mIsRunning;

        ScrollChecker() {
            mScroller = new Scroller(getContext());
        }

        @Override
        public void run() {
            boolean isFinish = !mScroller.computeScrollOffset() || mScroller.isFinished();
            int curY = mScroller.getCurrY();
            if (!isFinish) {
                movePos(curY + mStart);
                postDelayed(this, MOVE_DELAY);
            } else {
                reset();
            }
        }

        private void reset() {
            mIsRunning = false;
            mStart = 0;
        }

        void tryToScrollTo(int to, int duration) {
            if (mCurrentOffsetY == to) {
                return;
            }
            removeCallbacks(this);
            if (!mScroller.isFinished()) {
                mScroller.forceFinished(true);
            }
            mStart = mCurrentOffsetY;
            mScroller.startScroll(0, 0, 0, to - mStart, duration);
            post(this);
            mIsRunning = true;
        }

        void abortIfRunning() {
            if (mIsRunning) {
                if (!mScroller.isFinished()) {
                    mScroller.forceFinished(true);
                }
                reset();
            }
        }
    }

它的代码很简单,首先有一个Scroller,用于计算位移量。然后当触发回滚时,我们每12毫秒就执行我们的这个Runnable的回调,获取当前Scroller的结果,设置到位移中去。并且它还提供了一个方法abortIfRunning(),用于在回滚过程中当手指继续操作我们的LoadMoreLayout时让ContentView暂停下来。
最后,我们修改一下前面的代码,实现ContentView的归位。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnabled() || mContent == null || !mHasMore) {
            return super.onInterceptTouchEvent(ev);
        }

        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = ev.getY();
                mScrollChecker.abortIfRunning();//当手指继续按下时,取消回滚
                break;
        //...这里代码和前面一样
    }

    private void onRelease() {
        performLoadMore();
        mScrollChecker.tryToScrollTo(0, mDuration);
    }

最终成果

完整代码已经上传到Github,项目地址为:https://github.com/msdx/hi-loadmore
项目运行效果如下:
运行效果

后续扩展

我在前面提到,上拉加载的Footer可能不适合在LoadMoreLayout里实现,所以在我的实现当中也是不包含这一方面的代码的。一般可以实现LoadMoreUILayout接口,来自定义自己的FooterView。而对于像ListView或RecyclerView,个人倾向于使用ListView的FooterView或在RecyclerView的Adapter中添加FooterView来实现。后续会更新Github上的项目,补充对LoadMoreLayout的扩展以实现RecyclerView的上拉加载。但是否会再写一篇,视补充的内容多少而定,若可写内容较少或简单,则只更新项目。有相关疑问或建议请移步github该项目上提issue。

参考资料

作者:maosidiaoxian 发表于2017/9/24 19:09:22 原文链接
阅读:253 评论:0 查看评论

十分钟解决Android 6x以上系统运行时权限教程

$
0
0

大家好,今天给大家带来的是Android运行时权限处理办法。我们先来看看效果图。

当手机系统SDK版本在23以上,即Android6.0以后,在进行一些操作时候都需要对运行时权限进行处理。当然版本在23以上则无需担心。首先我们来认识下2个概念,一个安全权限,一个危险权限。安全权限就是系统默认的通过的权限,咱们只需要在配置文件中声明权限即可。而危险权限则需要用户在配置文件中声明并且需要进行授权处理,在UI交互方面的体验则是会给用户弹出授权窗口。


权限组的概念即当其中的一个权限被授权时候,该组的其他的权限也会被允许。这样就简化了我们在开发中对同组权限的个数,我们可以想象下,如果拨打电话和获取地理位置同时进行授权,则需要对该2组的所有权限进行申请,这就麻烦多了。

危险权限如下:

权限组 权限
CALENDAR
CAMERA
CONTACTS
LOCATION
MICROPHONE
PHONE
SENSORS
SMS
STORAGE

在明白了上述概念之后,我们来了解下为什么会出现申请授权的情况。在以往的23之前的版本中,系统在安装软件的时候会一次性把所有权限列出来。当然权限很多的时候还需要下拉才能看见。同时一些敏感的权限可能会导致用户不会去安装。所以就衍生出现在的版本。

我们需要注意的事情是用户在授权的时候会把近期授权给记录下来,意思就是授权过后一段时间之内,是会记住你的选择。从而不需要每次都会询问你是否需要授权。下面咱们将实战演练一个demo。

第一步:创建一个项目。配置文件中声明拨打电话的权限。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

第二步:编写布局文件。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.MainActivity">

    <EditText
        android:id="@+id/edt_call"
        android:layout_width="368dp"
        android:layout_height="wrap_content"
        android:hint="请输入电话号码"
        android:maxLines="1"
        />
    <Button
        android:layout_below="@id/edt_call"
        android:id="@+id/btn_call_click"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="拨打"
        />

</RelativeLayout>

展示效果如下:





第三步:编写拨打电话逻辑代码,实现不同版本的授权。


public class MainActivity extends AppCompatActivity {
    private Button btn_call_click;
    private EditText edt_call;
    private static final int MY_PERSSION_CALL_PHONE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        btn_call_click = (Button) findViewById(R.id.btn_call_click);
        edt_call = (EditText) findViewById(R.id.edt_call);
        btn_call_click.setOnClickListener(new MyOnclickListener());
    }

    private class MyOnclickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                //点击拨打电话按钮
                case R.id.btn_call_click:
                    //判断sdk版本
                    if (Build.VERSION.SDK_INT >= 23) {
                        //需要进行授权申请,首先判断该权限是否获取
                        int checkSelPermission = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE);
                        //进行判断
                        if (checkSelPermission != PackageManager.PERMISSION_GRANTED) {
                            //没有授权,即将进行授权
                            //先向用户解释为什么要进行授权
                            if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CALL_PHONE)) {
                                //此处会弹出一个对话框,解释为什么要授权。用户选择不再提醒或者拒绝。等待用户操作后再授权
                            } else {
                                //不向用户解释,即将授权
                                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, MY_PERSSION_CALL_PHONE);
                            }
                        } else {
                            //授权过了,可直接拨打电话
                           call();
                            return;
                        }
                    } else {
                        //系统在安装时候会自动提醒授权,否则不会安装该应用
                        call();
                        return;
                    }
                    break;
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            if(requestCode==MY_PERSSION_CALL_PHONE) {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call();
                } else {
                    Log.e("TAG","权限被禁止");
                }
            }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    private void call() {
        String phoneNum = edt_call.getText().toString();
        Intent intent = new Intent(Intent.ACTION_CALL);
        Uri data = Uri.parse("tel:" + phoneNum);
        intent.setData(data);
        startActivity(intent);
    }
}

由于代码中已经注释得很清楚,所以大家看注释应该能明白。如有不明白的地方,请发送问题到我的邮箱。另外,目前GitHub已经有多个库可以使用,本文仅限学习使用。
作者:qq_21004057 发表于2017/9/25 12:29:24 原文链接
阅读:34 评论:0 查看评论

渐变圆环进度条实现

$
0
0

      阅读本文章也不需要太多的自定义View基础,懂一点即可,这里的实现方式主要是逻辑,逻辑明白了,效果就很好做了。
说到环形进度条,我们必不可少的要用到的一个方法就是drawArc,这个是绘制弧度的必须方法,然后绘制谁都会,重要的是为什么会渐变呢?我们观察:

drawArc(Rect, Startangle, Sweepangle, false, Progresspaint)

      总共五个参数,第一个是方块模型,我们的弧形绘制都在此基础上,然后是Startangle,表示弧形开始绘制的起始角度,android里面是已顺时针为正数的,X正半轴为0,Sweepangle表示我们弧形画出的角度大小,第四个参数表示我们绘制弧度的时候要不要弧形两端和圆心连接起来,第五个参数就是我们的画笔了,好了,基本的使用方法大家已经了解了,那么可以开始了我们的思路。
      要使有渐变效果,然后又是弧形,正好弧形绘制方法里面有个画笔,那么我们只要不停的控制画笔的颜色不就可以了,说做就做,怎么做到这一点呢。
那开始绘制我们想要的效果之前,我们一定会给我们的弧形设两个基本值,那就是弧形起点的颜色,和结尾的颜色,所以中间的哪些颜色都是根据这两个颜色进行计算得来的,好了,拿到这两个值,我们可以开始计算每个位置的颜色,我们设计出一个获取颜色的方法

public int getGradient(float currentprogress, int startColor, int endColor)

我们这里有三个参数,第一个就是我们弧形绘制的进度,第二个和第三个是弧形的起始和结尾颜色,那么我们可以随时控制位置来获取颜色,具体获取方式如下:

        if (currentprogress > 1)
            currentprogress = 1;  //当前的进度值,最高不会超过1,这个值取自当前划过的角度和除以总的角度
        int alphaStart = Color.alpha(startColor);
        int redStart = Color.red(startColor);
        int blueStart = Color.blue(startColor);
        int greenStart = Color.green(startColor);
        int alphaEnd = Color.alpha(endColor);
        int redEnd = Color.red(endColor);
        int blueEnd = Color.blue(endColor);
        int greenEnd = Color.green(endColor);
        int alphaDifference = alphaEnd - alphaStart;
        int redDifference = redEnd - redStart;
        int blueDifference = blueEnd - blueStart;
        int greenDifference = greenEnd - greenStart;
        int alphaCurrent = (int) (alphaStart + currentprogress * alphaDifference);
        int redCurrent = (int) (redStart + currentprogress * redDifference);
        int blueCurrent = (int) (blueStart + currentprogress * blueDifference);
        int greenCurrent = (int) (greenStart + currentprogress * greenDifference);
        return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent);  //返回当前弧度该显示的颜色

      我们可以发现进度最大是1,也就是弧形绘制完毕,其他都是正处于绘制中,这里我们分别把起始和结尾颜色给拆开了,拆成了四个参数,分别是透明度和三原色,然后计算二者的差值,最后乘以进度再加上初始颜色,即可获取我们想要的颜色。
获取颜色的问题解决了,那怎么获取绘制进度呢。
从上面我们就可以知道,照我们这样绘制的话,我们绘制出来的弧形一定是一段一段连接起来的,所以我们会用到循环,下面给出代码:

        for (int i = 0, end = (int) (currentprogress * drawAngle); i < end; i++){
        progresspaint.setColor(getGradient(i / (float)end, startColor, endColor));
        canvas.drawArc(rectf, startangle + i, 1, false, progresspaint);

注释上面几个参数:

    private float drawAngle;          //我们计算得来的值
    private int currentprogress = 0;   //当前的进度
    private int allprogress = 100;     //总的进度

      这里只有两个变量,一个i,一个是currentprogress ,据此我们可以根据进度进行绘制,而且每次绘制都是一小段。那么怎么才能不断增长我们的进度呢?
我们可以在onDraw里面做处理,具体如下:

       drawArc(canvas); //开始绘制弧度
        if (currentprogress < allprogress) {   //如果绘制进度没有完成,那么这里每次都会判断一次,接着绘制
            currentprogress++;//进度增加
            postInvalidate();//重新进行下一次的绘制
        }

      只要发现当前进度没有完成,就会接着绘制,然后传递的currentprogress 就会一次次的发现变化,也就产生的所谓的动画效果,当然我们这个控件是需要自定义属性的,同学们可以自行设置需要动态设置的属性,我总共设置了以下自定义属性:

    private int startColor;          //起始颜色
    private int endColor;            //结束颜色
    private float sweepangle;          //划过角度
    private int startangle;          //起始角度
    private int currentprogress = 0;   //当前的进度
    private int allprogress = 100;        //总的进度
    private RectF rectf;              //绘制圆环所依赖的方块
    private float progresswidth;      //圆环宽度
    private Paint progresspaint;    //圆环的画笔

      自定义属性的获取和设置默认在代码里面。
      好了,总的难点就是这样,下面给出全部代码,方便粘贴,如果不能使用,最下面还有Demo。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by 23811 on 2017/09/25.
 */

public class ProgressColor extends View {

    private int startColor;          //起始颜色
    private int endColor;            //结束颜色
    private float sweepangle;          //划过角度
    private int startangle;          //起始角度
    private int currentprogress = 0;   //当前的进度
    private int allprogress = 100;        //总的进度
    private RectF rectf;              //绘制圆环所依赖的方块
    private float progresswidth;      //圆环宽度
    private Paint progresspaint;    //圆环的画笔
    private float drawAngle;


    //@Nullable这个注解的意思时,自定义的属性可以为空,这样的话,我们使用的时候,不去写,也不会报错
    public ProgressColor(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //获取各个自定义属性,并且初始化默认属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressColor);
        startColor = ta.getColor(R.styleable.ProgressColor_startcolor, Color.argb(255, 255, 210, 117));   //进度条初始颜色
        endColor = ta.getColor(R.styleable.ProgressColor_endcolor, Color.argb(255, 255, 57, 216));    //进度条结束颜色
        sweepangle = ta.getInt(R.styleable.ProgressColor_sweepagle, 240);    //进度条将要滑动的角度
        startangle = ta.getInt(R.styleable.ProgressColor_startangle, 150);   //进度条起始角度
        progresswidth = ta.getDimension(R.styleable.ProgressColor_progresswidth, 8f);   //进度条宽度
        ta.recycle();   //回收自定义属性资源
        progresspaint = new Paint();
        progresspaint.setAntiAlias(true);               //设置抗锯齿
        progresspaint.setStrokeWidth(progresswidth);  //设置圆环宽度
        progresspaint.setStyle(Paint.Style.STROKE);    //设置填充方式为空心
        progresspaint.setStrokeCap(Paint.Cap.ROUND);    //设置圆环弧度两边样式,取值有Cap.ROUND(圆形线冒)、Cap.SQUARE(方形线冒)、Paint.Cap.BUTT(无线冒)
        drawAngle = (float) (sweepangle / 100.0);
}

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int mMeasureWidth = getMeasuredWidth();
        int mMeasureHeight = getMeasuredHeight();
        //根据控件的大小建造绘制弧度的方块模型
        rectf = new RectF(getPaddingLeft() + progresswidth, getPaddingTop() + progresswidth,
                mMeasureWidth + getPaddingRight() - progresswidth,
                mMeasureHeight + getPaddingBottom() - progresswidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawArc(canvas); //开始绘制弧度
        if (currentprogress < allprogress) {   //如果绘制进度没有完成,那么这里每次都会判断一次,接着绘制
            currentprogress++;//进度增加
            postInvalidate();//重新进行下一次的绘制
        }
    }

    //绘制进度条
    private void drawArc(Canvas canvas) {
        //当currentprogress == 100时,也就是1 * drawAngle,完成全部绘制,从进度我们可以知道会这样一直绘制100次,每次绘制增加一点度数
        for (int i = 0, end = (int) (currentprogress * drawAngle); i < end; i++){
        //动态设置画笔的颜色,从而达到渐变的效果
        //下面有个地方需要注意,那就是i / (float)end ,这个部分的float时不可以少的,因为本身end就是一个很小的带分数的个位数,再被取整的话,就
        // 会看不见颜色的渐变了
        progresspaint.setColor(getGradient(i / (float)end, startColor, endColor));
        //下面五个参数分别是:绘制弧度需要的方形模型,绘制的起始角度,每次绘制的角度大小,绘制时是否将圆形和弧度两边连起来,以及绘制所需要的paint
        canvas.drawArc(rectf, startangle + i, 1, false, progresspaint);
        }
    }

    //这个方法的作用在于从起始和结束颜色中取三原色和透明度,然后计算得出当前划过角度的位置应该显示什么颜色
    public int getGradient(float currentprogress, int startColor, int endColor) {
        if (currentprogress > 1)
            currentprogress = 1;  //当前的进度值,最高不会超过1,这个值取自当前划过的角度和除以总的角度
        int alphaStart = Color.alpha(startColor);
        int redStart = Color.red(startColor);
        int blueStart = Color.blue(startColor);
        int greenStart = Color.green(startColor);
        int alphaEnd = Color.alpha(endColor);
        int redEnd = Color.red(endColor);
        int blueEnd = Color.blue(endColor);
        int greenEnd = Color.green(endColor);
        int alphaDifference = alphaEnd - alphaStart;
        int redDifference = redEnd - redStart;
        int blueDifference = blueEnd - blueStart;
        int greenDifference = greenEnd - greenStart;
        int alphaCurrent = (int) (alphaStart + currentprogress * alphaDifference);
        int redCurrent = (int) (redStart + currentprogress * redDifference);
        int blueCurrent = (int) (blueStart + currentprogress * blueDifference);
        int greenCurrent = (int) (greenStart + currentprogress * greenDifference);
        return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent);  //返回当前弧度该显示的颜色
    }

}

      我们发现进度满值是100,所以会重复绘制100次,这意味着什么?意味着我们修改这个值就能改变我们动画的速率,不信同学们自己去试试。
      其他注释也很详细,就不多说了,使用也很简单,直接设置宽高就可以使用,其他属性自定可以去看一下对照着添加,最后,Demo:

作者:wanxuedong 发表于2017/9/25 20:59:24 原文链接
阅读:0 评论:0 查看评论

19. Remove Nth Node From End of List。

$
0
0

Given a linked list, remove the nth node from the end of list and return its head.

For example,

Given linked list: 1->2->3->4->5, and n = 2.

After removing the second node from the end, the linked list becomes 1->2->3->5.


给定一个链表和数字n,要删除掉倒数第n个元素。最开始使用了stack,先遍历一遍原先的链表,然后依次放入栈中,再遍历栈并且使用头插法将栈中的元素一次组合起来,但是此刻需要对n做自减,当n等于0的时候就说明需要删除这个节点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if(!head) {
            return NULL;
        }

        stack<ListNode*> nodes;
        ListNode* result = new ListNode(0);

        while(head) {
            nodes.push(head);
            head = head->next;
        }

        while(!nodes.empty()) {
            n--;
            if(!n) {
                nodes.pop();
                if(nodes.empty()) break;
            }

            nodes.top()->next = result->next;
            result->next = nodes.top();
            nodes.pop();
        }
        return result->next;
    }
};

使用这一种方法可以达到这个级别。

这里写图片描述


第二种方法就是使用两个指针,然后用其中一个(first)先行遍历,遍历n各节点。然后两个指针(first和second)再同时遍历,遍历的终止条件就是第一个(first)指针到达尾部。那这样的话因为first最开始已经遍历了n步了,所以当first到达尾部的时候,second正好还差n步到达尾部,所以此刻就找到了所需要删除的地方。就好比两个人走一百步,第一个人先走了10步,然后两个人再同时走90步,这时候第一个人已经走了100步,而第二个走了90步,正好是差10步走完。利用这个原理,然后在调整其中的一些细节,比如说为了防止空指针的出现可以使用一个自己制造的头结点。

#include <iostream>
#include <stack>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {


        if(!head) {
            return NULL;
        }

        ListNode* result = new ListNode(0);
        result->next = head;
        ListNode* first = result;
        ListNode* second = result;

        n++;
        while(n) {
            n--;
            first = first->next;
        }

        while(first) {//直到first走到了尾部,那么first下一个就是我们需要删除的节点
            first = first->next;
            second = second->next;
        }

        second->next = second->next->next;//删除一个节日

        return result->next;


        /*if(!head) {
            return NULL;
        }

        stack<ListNode*> nodes;
        ListNode* result = new ListNode(0);

        while(head) {
            nodes.push(head);
            head = head->next;
        }

        while(!nodes.empty()) {
            n--;
            if(!n) {
                nodes.pop();
                if(nodes.empty()) break;
            }

            nodes.top()->next = result->next;
            result->next = nodes.top();
            nodes.pop();
        }
        return result->next;*/
    }
};

int main() {
    Solution s;


    ListNode node1(1);
    ListNode node2(2);
    ListNode node3(3);
    ListNode node4(4);
    ListNode node5(5);

    node1.next = &node2;
    node2.next = &node3;
    node3.next = &node4;
    node4.next = &node5;

    ListNode* p = s.removeNthFromEnd(&node1,2);

    while(p) {
        cout << p->val;
        cout << endl;
        p = p->next;
    }

}

参考资料:https://leetcode.com/articles/remove-nth-node-end-list/


按理说这个比第一种方法快一点,因为第一种遍历了两次,这个只遍历了一次,但是运行结果一样。

这里写图片描述

作者:Leafage_M 发表于2017/9/25 21:41:02 原文链接
阅读:0 评论:0 查看评论

147. Insertion Sort List。

$
0
0

Sort a linked list using insertion sort.


使用插入排序来排序链表。可以按照数组中的思路来做,先将创建一个新的链表,然后每次将原来的链表中的第一个节点拿出来与新的链表做对比,需要比较head的值和新建链表中每个值,直到找到合适的位置插入即可。

#include <iostream>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* insertionSortList(ListNode* head) {
        if(!head || !head->next) {
            return head;
        }

        ListNode* result = new ListNode(0);
        ListNode* temp;
        ListNode* tail;

        while(head) {
            temp = head->next;//将第一个节点隔离开来用来计算
            tail = result;//从新链表的第一个节点开始遍历

            while(tail->next && tail->next->val < head->val) {//将tail移动到所要插入节点的前面
                tail = tail->next;
            }

            head->next = tail->next;//将节点插入到中间
            tail->next = head;

            head = temp;
        }

        return result->next;
    }
};

int main() {
    Solution s;


    ListNode node1(10);
    ListNode node2(2);
    ListNode node3(31);
    ListNode node4(14);
    ListNode node5(5);

    node1.next = &node2;
    node2.next = &node3;
    node3.next = &node4;
    node4.next = &node5;

    ListNode* p = s.insertionSortList(&node1);

    while(p) {
        cout << p->val;
        cout << endl;
        p = p->next;
    }

}

作者:Leafage_M 发表于2017/9/25 21:45:46 原文链接
阅读:1 评论:0 查看评论

NVIDIA Jetson TX1 系列开发教程之十三:使用OpenCV在图像上添加汉字

$
0
0

NVIDIA Jetson TX1 系列开发教程之十三:使用OpenCV在图像上添加汉字




  各位看客注意了,本章教程无关硬件平台,在ubuntu下具有通用性,别被标题赶跑了哦。我踩过的坑,您就不要再踩了,枪在手,跟我走。有问题欢迎评论区留言~

  系列教程持续发布中,欢迎订阅、关注、收藏、评论、点赞哦~~( ̄▽ ̄~)~


前言

  同样的,由于项目需要,WordZzzz最近尝试在TX1上实现使用Opencv将解析json后得到的信息添加到图片上,其中包括汉字。然而,事情并没我想象的那么简单,主要还是自己对编码格式这块不太熟悉。下面将分两部分进行介绍。

安装freetype

  首先我们要检查一下自己的系统下面有没有freetype这个软件包:

$ pkg-config --cflags --libs freetype2
-I/usr/include/freetype2 -lfreetype

  如果有上述第二行的打印信息,那么这部分可以跳过,如果没有,就需要按照下面的步骤进行安装了。

#打开终端,添加PPA源:

$ sudo add-apt-repository ppa:no1wantdthisname/ppa
$ freetype-ppa

#安装freetype有两种方式:

$ upgrade freetype

#或者:

$ sudo apt update && sudo apt install libfreetype6

#如果你想卸载,科一进行如下操作:

$ sudo apt install ppa-purge && sudo ppa-purge ppa:no1wantdthisname/ppa

UTF-8与GBK的转换

ubuntu下系统默认的各种编码格式都是UTF-8,我们想要系统支持GBK,想让程序支持GBK,需要进行如下操作,我采用的方案二。

方案一:iconv函数族

  iconv函数族的头文件是iconv.h,使用前需包含。

#include <iconv.h>

  iconv函数族有三个函数,原型如下:
- (1) iconv_t iconv_open(const char *tocode, const char *fromcode);
此函数说明将要进行哪两种编码的转换,tocode是目标编码,fromcode是原编码,该函数返回一个转换句柄,供以下两个函数使用。
- (2) size_t iconv(iconv_t cd,char **inbuf,size_t *inbytesleft,char **outbuf,size_t *outbytesleft);
此函数从inbuf中读取字符,转换后输出到outbuf中,inbytesleft用以记录还未转换的字符数,outbytesleft用以记录输出缓冲的剩余空间。
- (3) int iconv_close(iconv_t cd);
此函数用于关闭转换句柄,释放资源。

  实现代码如下:

#include <iconv.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <string.h>  
#include <sys/stat.h>  

int code_convert(char *from_charset, char *to_charset, char *inbuf, size_t inlen,  
        char *outbuf, size_t outlen) {  
    iconv_t cd;  
    char **pin = &inbuf;  
    char **pout = &outbuf;  

    cd = iconv_open(to_charset, from_charset);  
    if (cd == 0)  
        return -1;  
    memset(outbuf, 0, outlen);  
    if (iconv(cd, pin, &inlen, pout, &outlen) == -1)  
        return -1;  
    iconv_close(cd);  
    *pout = '\0';  

    return 0;  
}  

int u2g(char *inbuf, size_t inlen, char *outbuf, size_t outlen) {  
    return code_convert("utf-8", "gb2312", inbuf, inlen, outbuf, outlen);  
}  

int g2u(char *inbuf, size_t inlen, char *outbuf, size_t outlen) {  
    return code_convert("gb2312", "utf-8", inbuf, inlen, outbuf, outlen);  
}  

int main(void) {  
    char *s = "中国";  
    int fd = open("test.txt", O_RDWR|O_CREAT, S_IRUSR | S_IWUSR);  
    char buf[10];  
    u2g(s, strlen(s), buf, sizeof(buf));  
    write(fd, buf, strlen(buf));  
    close(fd);  

    fd = open("test.txt2", O_RDWR|O_CREAT, S_IRUSR | S_IWUSR);  
    char buf2[10];  
    g2u(buf, strlen(buf), buf2, sizeof(buf2));  
    write(fd, buf2, strlen(buf2));  
    close(fd);  
    return 1;  
}  

方案二:使用mbstowcs和wcstombs

  mbstowcs将多字节编码转换为宽字节编码;wcstombs将宽字节编码转换为多字节编码
注意,需要系统编码的支持,可以通过locale -a 查看系统支持的。若不支持zh_CN.gbk, 需要安装,在ubuntu上的安装步骤如下:

  编辑配置文件:

$sudo vim /var/lib/locales/supported.d/zh-hans

  加入下面几行代码:

zh_CN.UTF-8 UTF-8
zh_SG.UTF-8 UTF-8
zh_CN.GBK GBK
zh_CN.GB18030 GB18030

  保存后更新:

$ sudo locale-gen

  查看:

$ locale -a
C
POSIX
···
zh_CN.gb18030
zh_CN.gbk
zh_CN.utf8
zh_SG.utf8

  测试代码:

#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <locale.h>  

/** 
 * DESCRIPTION: 实现由utf8编码到gbk编码的转换 
 * 
 * Input: gbkStr,转换后的字符串;  srcStr,待转换的字符串; maxGbkStrlen, gbkStr的最 
 大长度 
 * Output: gbkStr 
 * Returns: -1,fail;>0,success 
 * 
 */  
int utf82gbk(char *gbkStr, const char *srcStr, int maxGbkStrlen) {  
    if (NULL == srcStr) {  
        printf("Bad Parameter:srcStr\n");  
        return -1;  
    }  

    //首先先将utf8编码转换为unicode编码  
    if (NULL == setlocale(LC_ALL, "zh_CN.utf8")) //设置转换为unicode前的码,当前为utf8编码  
            {  
        printf("Bad Parameter:zh_CN.utf8\n");  
        return -1;  
    }  

    int unicodeLen = mbstowcs(NULL, srcStr, 0); //计算转换后的长度  
    if (unicodeLen <= 0) {  
        printf("Can not Transfer!!!\n");  
        return -1;  
    }  
    wchar_t *unicodeStr = (wchar_t *) calloc(sizeof(wchar_t), unicodeLen + 1);  
    mbstowcs(unicodeStr, srcStr, strlen(srcStr)); //将utf8转换为unicode  

    //将unicode编码转换为gbk编码  
    if (NULL == setlocale(LC_ALL, "zh_CN.gbk")) //设置unicode转换后的码,当前为gbk  
            {  
        printf("Bad Parameter:zh_CN.gbk\n");  
        return -1;  
    }  
    int gbkLen = wcstombs(NULL, unicodeStr, 0); //计算转换后的长度  
    if (gbkLen <= 0) {  
        printf("Can not Transfer!!!\n");  
        return -1;  
    } else if (gbkLen >= maxGbkStrlen) //判断空间是否足够  
            {  
        printf("Dst Str memory not enough\n");  
        return -1;  
    }  
    wcstombs(gbkStr, unicodeStr, gbkLen);  
    gbkStr[gbkLen] = 0; //添加结束符  
    free(unicodeStr);  
    return gbkLen;  
}  

int main(void) {  
    char *s = "我爱WordZzzz";  
    int fd = open("test.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
    char buf[10];  
    utf82gbk(buf, s, sizeof(buf));  
    write(fd, buf, strlen(buf));  
    close(fd);  

    return 1;  
}  

OpenCV的中文支持

  OpenCV想要支持中文显示,需要添加额外的文件。网上参考的都是这两个文件,2007年的,历史悠久。

  头文件CvxText.h:

#ifndef OPENCV_CVX_TEXT_2007_08_31_H
#define OPENCV_CVX_TEXT_2007_08_31_H

/**
* \file CvxText.h
* \brief OpenCV汉字输出接口
*
* 实现了汉字输出功能。
*/

#include <ft2build.h>
#include FT_FREETYPE_H

#include<opencv2/opencv.hpp>

/**
* \class CvxText
* \brief OpenCV中输出汉字
*
* OpenCV中输出汉字。字库提取采用了开源的FreeFype库。由于FreeFype是
* GPL版权发布的库,和OpenCV版权并不一致,因此目前还没有合并到OpenCV
* 扩展库中。
*
* 显示汉字的时候需要一个汉字字库文件,字库文件系统一般都自带了。
* 这里采用的是一个开源的字库:“文泉驿正黑体”。
*
* 关于"OpenCV扩展库"的细节请访问
* http://code.google.com/p/opencv-extension-library/
*
* 关于FreeType的细节请访问
* http://www.freetype.org/
*/


class CvxText
{
    // 禁止copy

    CvxText& operator=(const CvxText&);

    //================================================================
    //================================================================

public:

    /**
    * 装载字库文件
    */

    CvxText(const char *freeType);
    virtual ~CvxText();

    //================================================================
    //================================================================

    /**
    * 获取字体。目前有些参数尚不支持。
    *
    * \param font        字体类型, 目前不支持
    * \param size        字体大小/空白比例/间隔比例/旋转角度
    * \param underline   下画线
    * \param diaphaneity 透明度
    *
    * \sa setFont, restoreFont
    */

    void getFont(int *type,
        CvScalar *size = NULL, bool *underline = NULL, float *diaphaneity = NULL);

    /**
    * 设置字体。目前有些参数尚不支持。
    *
    * \param font        字体类型, 目前不支持
    * \param size        字体大小/空白比例/间隔比例/旋转角度
    * \param underline   下画线
    * \param diaphaneity 透明度
    *
    * \sa getFont, restoreFont
    */

    void setFont(int *type,
        CvScalar *size = NULL, bool *underline = NULL, float *diaphaneity = NULL);

    /**
    * 恢复原始的字体设置。
    *
    * \sa getFont, setFont
    */

    void restoreFont();

    //================================================================
    //================================================================

    /**
    * 输出汉字(颜色默认为黑色)。遇到不能输出的字符将停止。
    *
    * \param img  输出的影象
    * \param text 文本内容
    * \param pos  文本位置
    *
    * \return 返回成功输出的字符长度,失败返回-1。
    */

    int putText(IplImage *img, const char    *text, CvPoint pos);

    /**
    * 输出汉字(颜色默认为黑色)。遇到不能输出的字符将停止。
    *
    * \param img  输出的影象
    * \param text 文本内容
    * \param pos  文本位置
    *
    * \return 返回成功输出的字符长度,失败返回-1。
    */

    int putText(IplImage *img, const wchar_t *text, CvPoint pos);

    /**
    * 输出汉字。遇到不能输出的字符将停止。
    *
    * \param img   输出的影象
    * \param text  文本内容
    * \param pos   文本位置
    * \param color 文本颜色
    *
    * \return 返回成功输出的字符长度,失败返回-1。
    */

    int putText(IplImage *img, const char    *text, CvPoint pos, CvScalar color);

    /**
    * 输出汉字。遇到不能输出的字符将停止。
    *
    * \param img   输出的影象
    * \param text  文本内容
    * \param pos   文本位置
    * \param color 文本颜色
    *
    * \return 返回成功输出的字符长度,失败返回-1。
    */
    int putText(IplImage *img, const wchar_t *text, CvPoint pos, CvScalar color);

    //================================================================
    //================================================================

private:

    // 输出当前字符, 更新m_pos位置

    void putWChar(IplImage *img, wchar_t wc, CvPoint &pos, CvScalar color);

    //================================================================
    //================================================================

private:

    FT_Library   m_library;   // 字库
    FT_Face      m_face;      // 字体

    //================================================================
    //================================================================

    // 默认的字体输出参数

    int         m_fontType;
    CvScalar   m_fontSize;
    bool      m_fontUnderline;
    float      m_fontDiaphaneity;

    //================================================================
    //================================================================
};

#endif // OPENCV_CVX_TEXT_2007_08_31_H

  源文件cvText.cpp:

/*
 * cvText.cpp
 *
 *  Created on: Sep 20, 2017
 *      Author: wordzzzz
 */

#include <wchar.h>
#include <assert.h>
#include <locale.h>
#include <ctype.h>

#include "CvxText.h"

//====================================================================
//====================================================================

// 打开字库

CvxText::CvxText(const char *freeType)
{
    assert(freeType != NULL);

    // 打开字库文件, 创建一个字体

    if (FT_Init_FreeType(&m_library)) throw;
    if (FT_New_Face(m_library, freeType, 0, &m_face)) throw;

    // 设置字体输出参数

    restoreFont();

    // 设置C语言的字符集环境

    setlocale(LC_ALL, "");
}

// 释放FreeType资源

CvxText::~CvxText()
{
    FT_Done_Face(m_face);
    FT_Done_FreeType(m_library);
}

// 设置字体参数:
//
// font         - 字体类型, 目前不支持
// size         - 字体大小/空白比例/间隔比例/旋转角度
// underline   - 下画线
// diaphaneity   - 透明度

void CvxText::getFont(int *type, CvScalar *size, bool *underline, float *diaphaneity)
{
    if (type) *type = m_fontType;
    if (size) *size = m_fontSize;
    if (underline) *underline = m_fontUnderline;
    if (diaphaneity) *diaphaneity = m_fontDiaphaneity;
}

void CvxText::setFont(int *type, CvScalar *size, bool *underline, float *diaphaneity)
{
    // 参数合法性检查

    if (type)
    {
        if (type >= 0) m_fontType = *type;
    }
    if (size)
    {
        m_fontSize.val[0] = fabs(size->val[0]);
        m_fontSize.val[1] = fabs(size->val[1]);
        m_fontSize.val[2] = fabs(size->val[2]);
        m_fontSize.val[3] = fabs(size->val[3]);
    }
    if (underline)
    {
        m_fontUnderline = *underline;
    }
    if (diaphaneity)
    {
        m_fontDiaphaneity = *diaphaneity;
    }
    //FT_Set_Pixel_Sizes(m_face, (int)m_fontSize.val[0], 0);
}

// 恢复原始的字体设置

void CvxText::restoreFont()
{
    m_fontType = 0;            // 字体类型(不支持)

    m_fontSize.val[0] = 20;      // 字体大小
    m_fontSize.val[1] = 0.5;   // 空白字符大小比例
    m_fontSize.val[2] = 0.1;   // 间隔大小比例
    m_fontSize.val[3] = 0;      // 旋转角度(不支持)

    m_fontUnderline = false;   // 下画线(不支持)

    m_fontDiaphaneity = 1.0;   // 色彩比例(可产生透明效果)

    // 设置字符大小

    FT_Set_Pixel_Sizes(m_face, (int)m_fontSize.val[0], 0);
}

// 输出函数(颜色默认为黑色)

int CvxText::putText(IplImage *img, const char    *text, CvPoint pos)
{
    return putText(img, text, pos, CV_RGB(255, 255, 255));
}
int CvxText::putText(IplImage *img, const wchar_t *text, CvPoint pos)
{
    return putText(img, text, pos, CV_RGB(255, 255, 255));
}

//

int CvxText::putText(IplImage *img, const char    *text, CvPoint pos, CvScalar color)
{
    if (img == NULL) return -1;
    if (text == NULL) return -1;

    //

    int i;
    for (i = 0; text[i] != '\0'; ++i)
    {
        wchar_t wc = text[i];

        // 解析双字节符号

        if (!isascii(wc)) mbtowc(&wc, &text[i++], 2);

        // 输出当前的字符

        putWChar(img, wc, pos, color);
    }
    return i;
}
int CvxText::putText(IplImage *img, const wchar_t *text, CvPoint pos, CvScalar color)
{
    if (img == NULL) return -1;
    if (text == NULL) return -1;

    //

    int i;
    for (i = 0; text[i] != '\0'; ++i)
    {
        // 输出当前的字符

        putWChar(img, text[i], pos, color);
    }
    return i;
}

// 输出当前字符, 更新m_pos位置

void CvxText::putWChar(IplImage *img, wchar_t wc, CvPoint &pos, CvScalar color)
{
    // 根据unicode生成字体的二值位图

    FT_UInt glyph_index = FT_Get_Char_Index(m_face, wc);
    FT_Load_Glyph(m_face, glyph_index, FT_LOAD_DEFAULT);
    FT_Render_Glyph(m_face->glyph, FT_RENDER_MODE_MONO);

    //

    FT_GlyphSlot slot = m_face->glyph;

    // 行列数

    int rows = slot->bitmap.rows;
    int cols = slot->bitmap.width;

    //

    for (int i = 0; i < rows; ++i)
    {
        for (int j = 0; j < cols; ++j)
        {
            int off = ((img->origin == 0) ? i : (rows - 1 - i))
                * slot->bitmap.pitch + j / 8;

            if (slot->bitmap.buffer[off] & (0xC0 >> (j % 8)))
            {
                int r = (img->origin == 0) ? pos.y - (rows - 1 - i) : pos.y + i;;
                int c = pos.x + j;

                if (r >= 0 && r < img->height
                    && c >= 0 && c < img->width)
                {
                    CvScalar scalar = cvGet2D(img, r, c);

                    // 进行色彩融合

                    float p = m_fontDiaphaneity;
                    for (int k = 0; k < 4; ++k)
                    {
                        scalar.val[k] = scalar.val[k] * (1 - p) + color.val[k] * p;
                    }

                    cvSet2D(img, r, c, scalar);
                }
            }
        } // end for
    } // end for

    // 修改下一个字的输出位置

    double space = m_fontSize.val[0] * m_fontSize.val[1];
    double sep = m_fontSize.val[0] * m_fontSize.val[2];

    pos.x += (int)((cols ? cols : space) + sep);
}

  测试代码:

#include <opencv/highgui.h>  
#include <assert.h>  
#include "CvxText.h"  
using namespace cv;  

int main(int argc, char *argv[])  
{  
    // 打开一幅  
    IplImage *img = cvLoadImage("lena.jpg");  
    // 输出汉字  
    {  
        CvxText text("simhei.ttf"); // "zenhei.ttf"为黑体常规  
        const char *msg = "在OpenCV中输出汉字!";  
        float p = 0.5;  
        text.setFont(NULL, NULL, NULL, &p);   // 透明处理  
        text.putText(img, msg, cvPoint(100, 150), CV_RGB(255, 0, 0));  
    }  
    // 定义窗口,并显示影象  
    cvShowImage("test", img); cvWaitKey(-1);  
    cvReleaseImage(&img);  
    return 0;  
} 

  代码里面的simhei.tff是字体名称,需要拷贝到你程序的执行目录下,当然也可以直接在代码中写字体的路径。window下一般都有这个字体,ubuntu的话,来windows下拷贝一下就好,windows的的字体目录:C:\Windows\Fonts。

敲重点

  如果你的代码的编码格式本身就是GBK,那么你就不需要UTF-8转GBK这个过程,即第二个部分就可以略过;但是,如果你进行第三部分的时候,发现部分汉字能显示,部分不能显示,那就是编码问题,需要调用第二部分的UTF-8转GBK的函数。/font>


系列教程持续发布中,欢迎订阅、关注、收藏、评论、点赞哦~~( ̄▽ ̄~)~

完的汪(∪。∪)。。。zzz


作者:u011475210 发表于2017/9/25 20:06:37 原文链接
阅读:29 评论:0 查看评论

S5PV210启动过程分析

$
0
0
一、iROM启动方式简介
    九鼎X210开发板的SoC为三星S5PV210,S5PV210采用iROM启动方式进行启动,通过查阅三星官方S5PV210文档s5pv210_irom_applicationnote_preliminary可知,iROM启动方式支持从MoviNAND/iNand, MMC/SD Card ,pure Nand, eMMC, eSSD, UART and USB等存储设备启动。
1、硬件支持需求
使用iROM启动方式有几种硬件需求:
A、基于CortexA8的S5PV210X微处理器
B、64KB iROM
C、96KB iRAM
D、通用的SDRAM和控制器
E、4/8 Bit的高速SD/MMC控制器
F、4-bit SD / 4-bit MMC / 4 or 8-bit eMMC
G、Nand Flash控制器
H、OneNand控制器
I、eSSD控制器
J、UART/USB控制器
2、启动模式
A、OneNand启动(Mux/Demux)
B、Nand启动(支持8/16-Bit ECC)
C、MMC启动(MMC4.3标准,兼容eMMC)
D、eSSD启动
E、UART/USB启动
安全启动模式支持:对除UART/USB 外的所有启动设备的BootLoader采用整体校验,安全秘钥值写在S5PV210内部,如果安全秘钥值没有写在S5PV210内部,则为非安全启动模式。三星在制造阶段就将安全秘钥值写入到了S5PV210内部。
第二启动模式支持:当第一启动模式(安全启动模式)失败后,就会试图从4 bit的SD/MMC通道2通过SD/MMC启动。
3、iROM启动方式的优点
A、降低BOM(材料清单)成本
iROM启动支持从Movinand/iNAND/MMC/eMMC Card, eSSD设备启动,系统无需启动介质就可以启动,不需要像nor flash这样的启动设备。
B、改善读特性
采用iROM启动方式从nand flash启动时,S5PV210支持8/16-bit H/W ECC。但是16-bit ECC仅支持4KB 5cycle的Nand。
C、降低生产成本
能从其他启动设备烧录启动设备,无需Gang programmer烧录器
4、电路设计
A、使用OM引脚选择iROM启动设备
B、所有的S5PV210启动设备都可以从MMC通道2使用SD/MMC设备以第二启动方式启动。
C、OneNand 启动时,Xm0CSn4/NFCSn2/ONANDXL_CSn0信号必须有效。BL1代码段起始处必须是BL1的校验数据。
D、Nand启动时,Xm0CSn2/NFCSn0信号必须有效。BL1代码段起始处必须是BL1的校验数据。
E、SD/MMC/eMMC启动时,MMC通道0分配给第一启动模式使用,通道2分配给第二启动模式使用。BL1代码段起始处必须是BL1的校验数据。
F、UART启动使用串口端口2
二、iROM启动流程
S5PV210启动过程分为BL0、BL1、BL2三个阶段,S5PV210内部有96Kb的IRAM和64Kb的IROM。S5PV210启动过程如下图:

注释:(其中 BL1 最大 16KBBL2 最大 80KB,而后面我们自己移植的boot  200  Kbyte,因此就没法按照手册给的这个流程。我们实际的流程是:在 BL1 中初始化时钟、DRAM 控制器,拷贝 BL2到外部 DRAM,跳转到 DRAM 中执行 BL2BL2 加载 OS 到 OS 的起始地址执行 OS。)
第一步:iROM初始化,初始化系统时钟、特殊设备控制寄存器和启动设备
第二步:iROM启动代码加载BL1(bootloader)到iRAM,在安全启动模式下iROM对BL1进行整体校验。
第三步:执行BL1,BL1加载BL2(剩余的bootloader)到iRAM,BL1将会对BL2进行整体校验。
第四步:执行BL2,BL2初始化DRAM控制器,加载kernel到SDRAM
第五步:跳转到OS起始地址,进入系统
1、BL0启动阶段
在BL0阶段初始化的内容如下:
1、关闭看门狗
2、初始化icache
3、初始化栈(设置中断栈、SVC栈)
4、初始化堆
5、初始化块设备copy函数
6、初始化PLL和设置系统时钟
7、拷贝BL1到iram
8、校验BL1,如果校验失败,将从SD卡启动,(从启动设备中拷贝前16K的代码到IRAM的0xD0020000处;)
9、检查是否是安全启动模式
10、跳转到BL1的地址(0xD0020010
2、第一启动模式启动流程
安全启动模式(第一启动模式)的启动过程:

3、第二启动模式启动流程
第二启动模式的启动过程:

4、UART启动模式
S5PV210 iROM支持串口下载功能,串口下载一般通过发送校验位到DNW进行校验,不关心启动设备。对于串口下载来说不需要选择信号。为了避免串口超时错误,用户需要在开发板电源打开之前设置DNW配置,也就是当BL1代码被选中,下载进程启动时,iROM就应该被启动。
5、USB启动模式
S5PV210 iROM支持USB下载功能。如果串口超时发生,iROM会试图从USB模式启动。为了避免USB协商超时错误,用户必须预先用USB线连接好目标开发板和PC。如果USB连接好,用户就能通过USB线下载BL1镜像到开发板。
在UART、USB启动模式下BL1不需要头信息,BL1代码的基地址是0xD0020000,但是下载地址仍然为0xd0020010,因为 download完成后S5PV210会即刻跳转到0xd0020010处执行,那么如果下载到0xd0020000前面的16字的内容会被忽略。
在其他启动模式下,BL1必须有头信息,BL1代码的基地址是0xD0020010。
三、iROM启动的其他细节
1、iROM内存分配

2、全局变量
如果用MMC设备启动,MMC卡的信息必须保存在特殊区域。

2、设备拷贝函数
S5PV210内部有一段用于启动设备的块拷贝函数的ROM代码,因此开发者不需要实现设备拷贝函数。这些内部函数能从存储设备中拷贝任何数据到SDRAM,用户在iROM启动过程完成后仍然可以使用这些函数。


2、启动设备启动扇区分配
SD/MMC/eSSD设备启动扇区分配

我们需要按照上图的布局将程序烧写到 SD 卡,每个块大小为 512Byte。
Block0 保留, BL1 烧写到 Block1 开始的块,这是强制要求的。 BL2 和 Kernel 的布局要求只是建议。
eMMC设备启动扇区分配

OneNAND/NAND设备启动扇区分配

如果从Nand启动,Nand ECC数据应该项如下设定:

对于8bit ECC来说,ECC 数据大小是13字节
对于16bit ECC来说,ECC 数据大小是26字节,但是对于每一种Nand flash,ECC的大小是不一样的,因此需要去查阅Nand flash的数据手册。
2、启动代码的头信息数据
BL1必须有头信息数据,头信息数据是用于通过iROM复制BL1到iRAM中使用,头信息数据有两种信息,一种是BL1的大小,一种是BL1数据校验。当加载BL1时,iROM检查位于头数据中的BL1的大小,拷贝BL1到iRAM。拷贝完BL1后,iROM计算拷贝的BL1的数据和,与位于BL1头数据信息中的校验和数据进行比较。如果成功,BL1启动,否则iROM将会试图从SD/MMC通道2端口的第二启动模式(4-bit SD/MMC)启动。计算数据校验和的代码如下:
for(count=0;count< dataLength;count+=1)
{
buffer = (*(volatile u8*)(uBlAddr+count));
checkSum = checkSum + buffer;
}
Count:无符号整型
dataLength:BL1的大小
buffer:从BL1读取1字节数据的存储变量
Checksum:BL1的校验和

2、时钟设置
S5PV210只有24MHZ的外部晶体振荡器可用。

本博文翻译自:三星S5PV210官方S5PV210_iROM_ApplicationNote文档
作者:czg13548930186 发表于2017/9/25 21:26:32 原文链接
阅读:35 评论:0 查看评论

Kotlin总结3

$
0
0

Date

这个是日常用的比较多的类,在kotlin中用传统的方法,IDEA会提示语法警告,有更好的方法,就是下面的

  • 传统的方法
    fun formatDate(date: Date, pattern: String): String = SimpleDateFormat(pattern).format(date)

  • kotlin推荐方法
    fun formatDate(date: Date, dateFormat: DateFormat): String = dateFormat.format(date)

  • 提示如下

To get local formatting use getDateInstance(), getDateTimeInstance(), or getTimeInstance(), or use new SimpleDateFormat(String template, Locale locale) with for example Locale. US for ASCII dates.

获得本地格式化请使用getDateInstance(),getDateTimeInstance(),或者getTimeInstance(),或者使用new SimpleDateFormat(String template, Locale locale),例如Locale.US为ASCII日期。

  • 但是它只提供了常用的,这些之外的,还是自定义吧

静态常量和静态方法共存

  • 工具类

object DateUtils {

    const val SFStr = "yyyyMMdd"

    fun formatDate(date: Date, pattern: String): String = SimpleDateFormat(pattern).format(date)
    }
  • 引用
//java
DateUtils.INSTANCE.formatDate(new Date(),DateUtils.DF_YYYYMMDDHHMMSS);
//kotlin
DateUtils.formatDate(Date(),DateUtils.DF_YYYYMMDDHHMMSS)

java的引用,看起来是个单例,但是我的习惯是像java的静态方法一样调用
- class可以做到,如果把工具类改成class,那么静态常量就没法用了
- 伴生对象也可以做到,但object又不允许有伴生对象
- 所以,加个注解就搞定了@JvmStatic
- 加了注解不影响kotlin调用,只是简化了java调用

//工具类方法
@JvmStatic
fun formatDate(date: Date, pattern: String): String = SimpleDateFormat(pattern).format(date)

//java调用
DateUtils.formatDate(new Date(),DateUtils.DF_YYYYMMDDHHMMSS);

Kotlin读写流操作

写文件在java中是这么操作的

    public static void byteArrayToFile(byte[] bytes, String filePath) throws Exception {
        InputStream in = new ByteArrayInputStream(bytes);
        File destFile = new File(filePath);
        if (!destFile.getParentFile().exists()) {
            destFile.getParentFile().mkdirs();
        }
        destFile.createNewFile();
        OutputStream out = new FileOutputStream(destFile);
        byte[] cache = new byte[CACHE_SIZE];
        int nRead = 0;
        while ((nRead = in.read(cache)) != -1) {
            out.write(cache, 0, nRead);
            out.flush();
        }
        out.close();
        in.close();
    }

转成kotlin后,是不允许在while中写赋值表达式的,弄好好久,发现应该是这样的

    @Throws(Exception::class)
    fun byteArrayToFile(bytes: ByteArray, filePath: String) {
        val inStream = ByteArrayInputStream(bytes)
        val destFile = File(filePath)
        if (!destFile.parentFile.exists()) {
            destFile.parentFile.mkdirs()
        }
        destFile.createNewFile()
        val out = FileOutputStream(destFile)
        val cache = ByteArray(CACHE_SIZE)
        var nRead = inStream.read(cache)

        while (nRead != -1) {
            out.write(cache, 0, nRead)
            nRead = inStream.read(cache)
        }
        inStream.copyTo(out)
        out.close()
        inStream.close()
    }

然后Slient大神发了一个扩展方法InputStream.copyTo

于是就变成这样了

@Throws(Exception::class)
fun byteArrayToFile(bytes: ByteArray, filePath: String) {
    val inStream = ByteArrayInputStream(bytes)
    val destFile = File(filePath)
    if (!destFile.parentFile.exists())
        destFile.parentFile.mkdirs()
    destFile.createNewFile()
    val out = FileOutputStream(destFile)
    inStream.copyTo(out,MemoryUtils.KB)
    out.close()
    inStream.close()
}

卧槽,感觉好多语法糖,上次忘了一个什么方法,写了半天,也是Slient大神给了个语法糖

作者:lftaoyuan 发表于2017/9/25 11:05:10 原文链接
阅读:23 评论:0 查看评论

Android 8.0 功能和 API

$
0
0

Android 8.0 为用户和开发者引入多种新功能。本文重点介绍面向开发者的新功能。

用户体验

通知

在 Android 8.0 中,我们已重新设计通知,以便为管理通知行为和设置提供更轻松和更统一的方式。这些变更包括:
- 通知渠道:Android 8.0 引入了通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。用户界面将通知渠道称之为通知类别。要了解如何实现通知渠道的信息,请参阅通知渠道指南。
- 通知标志:Android 8.0 引入了对在应用启动器图标上显示通知标志的支持。通知标志可反映某个应用是否存在与其关联、并且用户尚未予以清除也未对其采取行动的通知。通知标志也称为通知点。要了解如何调整通知标志,请参阅通知标志指南。
- 休眠:用户可以将通知置于休眠状态,以便稍后重新显示它。重新显示时通知的重要程度与首次显示时相同。应用可以移除或更新已休眠的通知,但更新休眠的通知并不会使其重新显示。
- 通知超时:现在,使用 setTimeoutAfter() 创建通知时您可以设置超时。您可以使用此函数指定一个持续时间,超过该持续时间后,通知应取消。如果需要,您可以在指定的超时持续时间之前取消通知。
- 通知设置:当您使用Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCESIntent 从通知创建指向应用通知设置的链接时,您可以调用 setSettingsText() 来设置要显示的文本。此系统可以提供以下 Extra 数据和 Intent,用于过滤应用必须向用户显示的设置:EXTRA_CHANNEL_ID、NOTIFICATION_TAG 和 NOTIFICATION_ID。
- 通知清除:系统现在可区分通知是由用户清除,还是由应用移除。要查看清除通知的方式,您应实现 NotificationListenerService 类的新 onNotificationRemoved() 函数。
- 背景颜色:您现在可以设置和启用通知的背景颜色。只能在用户必须一眼就能看到的持续任务的通知中使用此功能。例如,您可以为与驾车路线或正在进行的通话有关的通知设置背景颜色。您还可以使用 Notification.Builder.setColor() 设置所需的背景颜色。这样做将允许您使用 Notification.Builder.setColorized() 启用通知的背景颜色设置。
- 消息样式:现在,使用 MessagingStyle 类的通知可在其折叠形式中显示更多内容。对于与消息有关的通知,您应使用 MessagingStyle 类。您还可以使用新的 addHistoricMessage() 函数,通过向与消息相关的通知添加历史消息为会话提供上下文。

用户可以长按应用启动器图标以查看 Android 8.0 中的通知

自动填充框架

帐号创建、登录和信用卡交易需要时间并且容易出错。在使用要求执行此类重复性任务的应用时,用户很容易遭受挫折。

Android 8.0 通过引入自动填充框架,简化了登录和信用卡表单之类表单的填写工作。在用户选择接受自动填充之后,新老应用都可使用自动填充框架。

您可以采取某些措施,优化您的应用使用此框架的方式。如需了解详细信息,请参阅自动填充框架概览。

画中画模式

Android 8.0 允许以画中画 (PIP) 模式启动操作组件。PIP 是一种特殊的多窗口模式,最常用于视频播放。目前,PIP 模式可用于 Android TV,而 Android 8.0 则让该功能可进一步用于其他 Android 设备。

当某个 Activity 处于 PIP 模式时,它会处于暂停状态,但仍应继续显示内容。因此,您应确保您的应用在 onPause() 处理程序中进行处理时不会暂停播放。相反,您应在 onStop() 中暂停播放视频,并在 onStart() 中继续播放。如需了解详细信息,请参阅多窗口生命周期。

要指定您的 Activity 可以使用 PIP 模式,请在清单中将 android:supportsPictureInPicture 设置为 true。(从 Android 8.0 开始,如果您打算在 Android TV 或其他 Android 设备上支持 PIP 模式,则无需将 android:resizeableActivity 设置为 true;只有在您的 Activity 支持其他多窗口模式时,才需要设置 android:resizeableActivity。)

API 变更

Android 8.0 引入一种新的对象 PictureInPictureParams,您可以将该对象传递给 PIP 函数来指定某个 Activity 在其处于 PIP 模式时的行为。此对象还指定了各种属性,例如操作组件的首选纵横比。

现在,在添加画中画中介绍的现有 PIP 函数可用于所有 Android 设备,而不仅限于 Android TV。此外,Android 8.0 还提供以下函数来支持 PIP 模式:
- Activity.enterPictureInPictureMode(PictureInPictureParams args):将操作组件置于画中画模式。操作组件的纵横比和其他配置设置均由 args 指定。如果 args 中的任何字段为空,系统将使用您上次调用 Activity.setPictureInPictureParams() 时所设置的值。
指定的操作组件被置于屏幕的一角,屏幕剩余部分则被屏幕显示的上一个操作组件填满。进入 PIP 模式的 Activity 将进入暂停状态,但仍保持已启动状态。如果用户点按此 PIP 操作组件,系统将显示一个菜单供用户操作,而在操作组件处于 PIP 状态期间,不会理会任何触摸事件。
- Activity.setPictureInPictureParams():更新操作组件的 PIP 配置设置。如果操作组件目前处于 PIP 模式,则会更新此设置;如果操作组件的纵横比发生变化,这非常有用。如果操作组件不处于 PIP 模式,则会使用这些配置设置,而不会考虑您调用的 enterPictureInPictureMode() 函数。

可下载字体

Android 8.0 和 Android 支持库 26 允许您从提供程序应用请求字体,而无需将字体绑定到 APK 中或让 APK 下载字体。此功能可减小 APK 大小,提高应用安装成功率,使多个应用可以共享同一种字体。

如需了解有关下载字体的详细信息,请参阅 可下载字体

XML 中的字体

Android 8.0 推出一项新功能,即 XML 中的字体,允许您使用字体作为资源。这意味着,不再需要以资产的形式捆绑字体。字体在 R 文件中编译,并且作为一种资源,可自动用于系统。然后,您可以利用一种新的资源类型 font 来访问这些字体。

在运行 API 版本 14 及更高版本的设备中,支持库 26 对此功能提供完全支持。

自动调整 TextView 的大小

Android 8.0 允许您根据 TextView 的大小自动设置文本展开或收缩的大小。这意味着,在不同屏幕上优化文本大小或者优化包含动态内容的文本大小比以往简单多了。如需了解有关如何在 Android 8.0 中自动调整 TextView 的大小的详细信息,请参阅自动调整 TextView 的大小。

自适应图标

Android 8.0 引入自适应启动器图标。自适应图标支持视觉效果,可在不同设备型号上显示为各种不同的形状。要了解如何创建自适应图标,请参阅自适应图标预览功能指南。

颜色管理

图像应用的 Android 开发者现在可以利用支持广色域彩色显示的新设备。要显示广色域图像,应用需要在其清单(每个操作组件)中启用一个标志,并加载具有嵌入的广域彩色配置文件(AdobeRGB、Pro Photo RGB、DCI-P3 等)的位图。

WebView API

Android 8.0 提供多种 API,帮助您管理在应用中显示网页内容的 WebView 对象。这些 API 可增强应用的稳定性和安全性,它们包括:
- Version API
- Google SafeBrowsing API
- Termination Handle API
- Renderer Importance API

固定快捷方式和小部件

Android 8.0 引入了快捷方式和微件的应用内固定功能。在您的应用中,您可以根据用户权限为支持的启动器创建固定的快捷方式和小部件。

如需了解详细信息,请参阅固定快捷方式和微件预览功能指南。

最大屏幕纵横比

以 Android 7.1(API 级别 25)或更低版本为目标平台的应用默认的最大屏幕纵横比为 1.86。针对 Android 8.0 或更高版本的应用没有默认的最大纵横比。如果您的应用需要设置最大纵横比,请使用定义您的操作组件的清单文件中的 maxAspectRatio 属性。

多显示器支持

从 Android 8.0 开始,此平台为多显示器提供增强的支持。如果 Activity 支持多窗口模式,并且在具有多显示器的设备上运行,则用户可以将 Activity 从一个显示器移动到另一个显示器。当应用启动 Activity 时,此应用可指定 Activity 应在哪个显示器上运行。

注:如果 Activity 支持多窗口模式,则 Android 8.0
将为该 Activity 自动启用多显示器支持。您应测试您的应用,
确保它在多显示器环境下可正常运行。

每次只有一个 Activity 可以处于继续状态,即使此应用具有多个显示器。具有焦点的 Activity 将处于继续状态,所有其他可见的 Activity 均暂停,但不会停止。如需了解有关当多个 Activity 可见时活动生命周期的详细信息,请参阅多窗口生命周期。

当用户将 Activity 从一个显示器移动到另一个显示器时,系统将调整 Activity 大小,并根据需要发起运行时变更。您的 Activity 可以自行处理配置变更,或允许系统销毁包含该 Activity 的进程,并以新的尺寸重新创建它。如需了解详细信息,请参阅处理配置变更。

ActivityOptions 提供两个新函数以支持多个显示器:
- setLaunchDisplayId() 指定 Activity 在启动后应显示在哪个显示器上。
- getLaunchDisplayId() 返回操作组件的当前启动显示器。

对 adb shell 进行了扩展,以支持多个显示器。shell start 命令现在可用于启动操作组件,并指定操作组件的目标显示器:

adb shell start <activity_name> --display <display_id>

统一的布局外边距和内边距

Android 8.0 让您可以更轻松地指定 View 元素的对边使用相同外边距和内边距的情形。具体来说,您现在可以在布局 XML 文件中使用以下属性:
- layout_marginVertical,同时定义 layout_marginTop 和 layout_marginBottom。
- layout_marginHorizontal,同时定义 layout_marginLeft 和 layout_marginRight。
- paddingVertical,同时定义 paddingTop 和 paddingBottom。
- paddingHorizontal,同时定义 paddingLeft 和 paddingRight。

注:如果您自定义应用逻辑以支持不同语言和文化(包括文本方向),
请记住,这些属性不会影响 layout_marginStart、layout_marginEnd、paddingStart 或 paddingEnd
的值。您可以自行设置这些值和新的垂直与水平布局属性
来创建取决于文本方向的布局行为。

指针捕获

某些应用(例如游戏、远程桌面和虚拟化客户端)将大大受益于鼠标指针控制。指针捕获是 Android 8.0 中的一项新功能,可以通过将所有鼠标事件传递到您的应用中焦点视图的方式提供此类控制。

从 Android 8.0 开始,您的应用中的 View 可以请求指针捕获并定义一个侦听器来处理捕获的指针事件。鼠标指针在此模式下将隐藏。如果不再需要鼠标信息,该视图可以释放指针捕获。系统也可以在视图丢失焦点时(例如,当用户打开另一个应用时)释放指针捕获。

应用类别

在适当的情况下,Android 8.0 允许每个应用声明其所属的类别。这些类别用于将应用呈现给用户的用途或功能类似的应用归类在一起,例如按流量消耗、电池消耗和存储消耗将应用归类。您可以在 清单标记中设置 android:appCategory 属性,定义应用的类别。

Android TV 启动器

Android 8.0 添加了一种以内容为中心的全新 Android TV 主屏幕体验,支持 Android TV 模拟器和 Nexus Player Android 8.0 设备映像。新的主屏幕在对应于频道的行中组织视频内容,这些频道在系统上通过应用填充各个节目。应用可以发布多个频道,用户可以配置他们希望在主屏幕上看到哪些频道。Android TV 也包含一个 Watch Next 行,此行根据用户的观看习惯从应用填充节目。应用也可以提供视频预览,这些预览会在用户聚焦到节目时自动播放。用于填充频道和节目的 API 属于 TvProvider API,这些 API 以 Android 支持库模块的形式随 Android 8.0 分发。

AnimatorSet

从 Android 8.0 开始,AnimatorSet API 现在支持寻道和倒播功能。寻道功能允许您将动画的位置设置为指定的时间点处。如果您的应用包含可撤消的操作的动画,倒播功能会很有用。现在,您不必定义两组独立的动画,而只需反向播放同一组动画。

输入和导航

键盘导航键区

如果您的应用中,某个操作组件使用一种复杂的视图层次结构(如图 2 所示),可考虑将多组界面元素组成一个键区,简化键盘导航这些元素的操作。用户可以在 Chromebook 设备上按 Meta+Tab 或 Search+Tab,在不同键区之间导航。键区的一些范例包括:侧面板、导航栏、主内容区域和可能包含多个子元素的元素。

以一个包含五个导航键区的操作组件为例,用户可以使用键盘导航键区快捷键进行导航。键区按以下布局显示:顶部面板、左侧面板、主内容区域、底部面板和浮动操作按钮。

要将一个 View 或 ViewGroup 元素设置为一个键区,请在元素的布局 XML 文件中将 android:keyboardNavigationCluster 属性设置为 true,或者将 true 传递至应用界面逻辑中的 setKeyboardNavigationCluster()。

注:键区不能嵌套,不过,非嵌套键区可以显示在层次结构的不同层级。
如果您尝试嵌套键区,框架仅会将最顶层的 ViewGroup 元素视为键区。

在具有触摸屏的设备中,您可以将某个键区指定的 ViewGroup 对象的 android:touchscreenBlocksFocus 元素设置为 true,仅允许从键区导航进入和离开此键区。如果您将此配置应用于某个键区,用户将无法使用 Tab 键或箭头键导航进入或离开此键区,而是必须按键区导航键盘组合键。

包含 5 个键区的操作组件

视图默认焦点

在 Android 8.0 中,您可以指定在(重新)创建的操作组件继续运行并且用户按下键盘导航键(例如 Tab 键)之后应接收焦点的 View。要应用“设为默认焦点”设置,请在包含界面元素的布局 XML 文件中将 View 元素的 android:focusedByDefault 属性设置为 true,或者将 true 传递至应用界面逻辑中的 setFocusedByDefault()。

系统

新的 StrictMode 检测程序

Android 8.0 添加了三个新的 StrictMode 检测程序,帮助识别应用可能出现的错误:
- detectUnbufferedIo() 将检测您的应用何时读取或写入未缓冲的数据,这可能极大影响性能。
- detectContentUriWithoutPermission() 将检测您的应用在其外部启动 Activity 时何时意外忘记向其他应用授予权限。
- detectUntaggedSockets() 将检测您的应用何时使用网络流量,而不使用 setThreadStatsTag(int) 将流量标记用于调试目的。

缓存数据

Android 8.0 优化了缓存数据的导航和行为。现在,每个应用均获得一定的磁盘空间配额,用于存储 getCacheQuotaBytes(UUID) 返回的缓存数据。

当系统需要释放磁盘空间时,将开始从超过配额最多的应用中删除缓存文件。因此,如果将您的缓存数据量始终保持低于配额的水平,则在必须清除系统中的某些文件时,您的缓存文件将能坚持到最后。系统在决定删除您的应用中的哪些缓存文件时,将首先考虑删除最旧的文件(由修改时间确定)。

您还可以针对每个目录启用两种新行为,以控制系统如何释放缓存数据:
- StorageManager.setCacheBehaviorAtomic() 可用于指示某个目录及其所有内容应作为一个不可分割的整体进行删除。
- setCacheBehaviorTombstone(File, boolean) 可用于指示不应删除某个目录内的文件,而应将它们截断到 0 字节长度,使空文件保持完好。

最后,在需要为大文件分配磁盘空间时,可考虑使用新的 allocateBytes(FileDescriptor, long) API,它将自动清除属于其他应用的缓存文件(根据需要),以满足您的请求。在确定设备是否有足够的磁盘空间保存您的新数据时,请调用 getAllocatableBytes(UUID) 而不要使用 getUsableSpace(),因为前者会考虑系统要为您清除的任何缓存数据。

内容提供程序分页

我们已更新内容提供程序以支持加载大型数据集,每次加载一页。例如,一个具有大量图像的照片应用可查询要在页面中显示的数据的子集。内容提供程序返回的每个结果页面由一个 Cursor 对象表示。客户端和提供程序必须实现分页才能利用此功能。

内容刷新请求

现在,ContentProvider 和 ContentResolver 类均包含 refresh() 函数,这样,客户端可以更轻松地知道所请求的信息是否为最新信息。

您可以扩展 ContentProvider 以添加自定义的内容刷新逻辑。请务必重写 refresh() 函数,以返回 true,告知提供程序的客户端您已尝试自行刷新数据。

您的客户端应用可通过调用另一个函数(又称 refresh()),显式请求已刷新的内容。在调用此函数时,传入待刷新数据的 URI。

注:由于您可能通过网络不断请求数据,
您应仅在有明显迹象表明内容确已过时时才从客户端调用 refresh()。
执行此类内容刷新最常见的原因是响应滑动刷新手势,
该手势显式请求当前界面显示最新内容。

JobScheduler 改进

Android 8.0 引入了对 JobScheduler 的多项改进。由于您通常可以使用计划作业替代现在受限的后台服务或隐式广播接收器,这些改进可以让您的应用更轻松地符合新的后台执行限制。

JobScheduler 的更新包括:
- 您现在可以将工作队列与计划作业关联。要将一个工作项添加到作业的队列中,请调用 JobScheduler.enqueue()。当作业运行时,它可以将待定工作从队列中剥离并进行处理。这种功能可以处理之前需要启动后台服务(尤其是实现 IntentService 的服务)的许多用例。
- 您现在可以通过调用 JobInfo.Builder.setClipData() 的方式将 ClipData 与作业关联。利用此选项,您可以将 URI 权限授予与作业关联,类似于这些权限传递到 Context.startService() 的方式。您也可以将 URI 权限授予用于工作队列上的 intent。
- 计划作业现在支持多个新的约束条件:JobInfo.isRequireStorageNotLow()
如果设备的可用存储空间非常低,作业将不会运行。
JobInfo.isRequireBatteryNotLow()
如果电池电量等于或低于临界阈值,作业将不会运行;临界阈值是指设备显示 Low battery warning 系统对话框的电量。
NETWORK_TYPE_METERED
作业需要一个按流量计费的网络连接,比如大多数移动数据网络数据套餐。

自定义数据存储

Android 8.0 允许您为首选项提供自定义数据存储,如果您的应用将首选项存储在云或本地数据库中,或者如果首选项特定于某个设备,此功能会非常有用。如需了解有关实现数据存储的详细信息,请参阅自定义数据存储。

findViewById() 签名变更

现在,findViewById() 函数的全部实例均返回 T,而不是 View。此变更会带来以下影响:
- 例如,如果 someMethod(View) 和 someMethod(TextView) 均接受调用 findViewById() 的结果,这可能导致现有代码的返回类型不确定。
- 在使用 Java 8 源语言时,这需要在返回类型不受限制时(例如,assertNotNull(findViewById(…)).someViewMethod()))显式转换为 View。
- 重写非最终的 findViewById() 函数(例如,Activity.findViewById())将需要更新其返回类型。

媒体增强功能

VolumeShaper

有一个新的 VolumeShaper 类。您可以用它来执行简短的自动音量转换,例如淡入、淡出和交叉淡入淡出。

音频焦点增强功能

音频应用通过请求和舍弃音频焦点的方式在设备上共享音频输出。应用通过启动或停止播放或者闪避音量的方式处理处于聚焦状态的变更。有一个新的 AudioFocusRequest 类。对于此类,应用在处理音频焦点变化时会使用新功能:自动闪避和延迟聚焦。

媒体指标

新的 getMetrics() 函数将返回一个包含配置和性能信息的 PersistableBundle 对象,用一个包含属性和值的地图表示。为以下媒体类定义 getMetrics() 函数:
- MediaPlayer.getMetrics()
- MediaRecorder.getMetrics()
- MediaCodec.getMetrics()
- MediaExtractor.getMetrics()

为每个实例单独收集指标,并持续到实例的生命周期结束为止。如果没有可用的指标,则此函数将返回 null。返回的实际指标取决于类。

MediaPlayer

Android 8.0 为 MediaPlayer 类添加了多种新函数。这些函数可以从多个方面增强您的应用处理媒体播放的能力:
- 在搜索帧时进行精细控制。
- 播放受数字版权管理保护的材料的功能。

MediaPlayer 现在支持采样级加密。

音频录制器

  • 音频录制器现在支持对流式传输有用的 MPEG2_TS 格式:mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS);
  • MediaMuxer 现在可以处理任意数量的音频和视频流,而不再仅限于一个音频曲目和/或一个视频曲目。使用 addTrack() 可混录所需的任意数量的曲目。
  • MediaMuxer 还可以添加一个或多个包含用户定义的每帧信息的元数据曲目。元数据的格式由您的应用定义。仅对 MP4 容器支持元数据曲目。

元数据可以用于离线处理。例如,传感器的陀螺仪信号可以用于执行视频稳定操作。

在添加元数据曲目时,曲目的 MIME 格式必须以前缀“application/”开头。除了数据不是来源于 MediaCodec 以外,写入元数据的操作与写入视频/音频数据相同。相反,应用将包含相关时间戳的 ByteBuffer 传递给 writeSampleData() 函数。时间戳必须和视频及音频曲目处于相同的时基。

生成的 MP4 文件使用 ISOBMFF 的 12.3.3.2 部分定义的 TextMetaDataSampleEntry,指示元数据的 MIME 格式。在使用 MediaExtractor 提取包含元数据曲目的文件时,元数据的 MIME 格式将提取到 MediaFormat 中。

音频播放控制

Android 8.0 允许您查询和请求设备产生声音的方式。对音频播放的以下控制将让您的服务更轻松地仅在有利的设备条件下产生声音。

Google 智能助理的新音频使用类型

AudioAttributes 类包含一种新的声音类型,即 USAGE_ASSISTANT,对应于 Google 智能助理在设备上的回答。

设备音频播放的变更

如果您希望自己的服务仅在特定的设备音频配置处于活动状态时开始产生声音,您可以使用 AudioManager 类注册一个 AudioManager.AudioPlaybackCallback 实例,后者的onPlaybackConfigChanged() 函数可以帮助您确定当前活动的音频属性集。

显式请求音频焦点

您的服务可以使用 requestAudioFocus() 函数提交一个更精细的设备级音频焦点接收请求。传入一个 AudioFocusRequest 对象,您可以使用 AudioFocusRequest.Builder 创建这个对象。在这个构建类中,您可以指定以下选项:

  • 您希望获得的焦点类型,例如 AUDIOFOCUS_GAIN_TRANSIENT 或 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK。
  • 当另一个音频服务获得设备焦点时,您的服务应以更安静的方式继续,还是完全暂停。
  • 您的服务能否等待获得焦点,直至设备就绪。

注:构建您的 AudioFocusRequest 实例时,如果您通过调用 setAcceptsDelayedFocusGain() 指示您的服务可以等待产生声音,您也必须调用 setOnAudioFocusChangeListener(),以便您的服务了解它何时可以开始产生声音。

增强的媒体文件访问功能

存储访问框架 (SAF) 允许应用显示自定义 DocumentsProvider,后者可以为其他应用提供访问数据源中的文件的权限。事实上,文档提供程序甚至可以提供驻留在网络存储区或使用媒体传输协议 (MTP) 等协议的文件的访问权限。

但是,访问远程数据源中的大媒体文件面临一些挑战:
- 媒体播放器需要以寻址方式访问来自文档提供程序的文件。当大媒体文件驻留在远程数据源上时,文档提供程序必须事先提取所有数据,并创建快照文件描述符。媒体播放器无法播放没有文件描述符的文件,因此在文档提供程序完成文件下载前,无法开始播放。
- 照片应用等媒体集合管理器必须通过作用域文件夹遍历一系列访问 URI 才能访问存储在外部 SD 卡上的媒体。这种访问模式会让媒体上的批量操作(例如移动、复制和删除)变得非常缓慢。
- 媒体集合管理器无法根据文档的 URI 确定其位置。这就让这些类型的应用难以允许用户选择媒体文件的保存位置。

Android 8.0 通过改进存储访问框架解决了各个挑战。

自定义文档提供程序

从 Android 8.0 开始,存储访问框架允许自定义文档提供程序为驻留在远程数据源中的文件创建可寻址的文件描述符。SAF 可打开文件,获取原生可寻址的文件描述符。然后 SAF 向文档提供程序提交离散字节请求。此功能使文档提供程序可以返回媒体播放器应用请求的准确字节范围,而不必事先缓存整个文件。

要使用此功能,您需要调用新的 StorageManager.openProxyFileDescriptor() 函数。openProxyFileDescriptor() 函数可接受 ProxyFileDescriptorCallback 对象作为回调。任何时候,当客户端应用对文档提供程序返回的文件描述符执行文件操作时,SAF 都会调用回调。

直接文档访问

从 Android 8.0 开始,您可以使用 getDocumentUri() 函数获得与给定 mediaUri 引用相同文档的 URI。不过,由于返回的 URI 由 DocumentsProvider 提供支持,媒体集合管理器可以直接访问文档,不用遍历作用域目录树。因此,媒体管理器能够以明显加快的速度对文档执行文件操作。

注意:getDocumentUri() 函数仅可以定位媒体文件;无法授予应用访问这些文件的权限。要详细了解如何获取媒体文件的访问权限,请参阅参考文档。

文档路径

在 Android 8.0 中使用存储访问框架时,您可以根据文档的 ID,使用 findDocumentPath() 函数(存在于 DocumentsContract 和 DocumentsProvider 类中)从文件系统的根目录中确定路径。该函数将在 DocumentsContract.Path 对象中返回此路径。如果文件系统对相同文档有多个定义的路径,该函数将返回访问具有给定 ID 的文档时最常使用的路径。

此功能在下列情况下特别有用:
- 您的应用使用可以显示特定文档位置的“另存为”对话框。
- 您的应用在搜索结果视图中显示文件夹并且如果用户选择某个文件夹,应用必须加载此特定文件夹内的子文档。

注:如果您的应用仅具有路径中某些文档的访问权限,那么 findDocumentPath() 的返回值将仅包含您的应用可以访问的文件夹和文档。

连接

WLAN 感知

Android 8.0 新增了对 WLAN 感知的支持,此技术基于周边感知联网 (NAN) 规范。在具有相应 WLAN 感知硬件的设备上,应用和附近设备可以通过 WLAN 进行搜索和通信,无需依赖互联网接入点。我们正在与硬件合作伙伴合作,以尽快将 WLAN 感知技术应用于设备。要了解有关如何将 WLAN 感知集成到您的应用中的信息,请参阅 WLAN 感知。

蓝牙

Android 8.0 通过增加以下功能,增强了平台对蓝牙的支持:
- 支持 AVRCP 1.4 标准,该标准支持音乐库浏览。
- 支持蓝牙低功耗 (BLE) 5.0 标准。
- 将 Sony LDAC 编解码器集成到蓝牙堆叠中。

配套设备配对

在尝试通过蓝牙、BLE 和 WLAN 与配套设备配对时,Android 8.0 提供的 API 允许您自定义配对请求对话框。如需了解详细信息,请参阅配套设备配对。

如需了解有关在 Android 上使用蓝牙的详细信息,请参阅蓝牙指南。有关对蓝牙所作的特定于 Android 8.0 的变更,请参阅 Android 8.0 行为变更页面的蓝牙部分。

共享

智能共享

Android 8.0 了解用户的个性化分享首选项,在通过哪些应用分享各个类型的内容方面,也有着更好的把握。例如,如果用户为一张收据拍照,Android 8.0 可以建议费用跟踪应用;如果用户自拍,一款社交媒体应用可以更好地处理图像。Android 8.0 可以根据用户的个性化首选项自动学习所有这些模式。

智能分享适用于 image 之外的内容类型,例如 audio、video、text 和 URL 等。

要启用智能分享,请将具有最多三个字符串注释的 ArrayList 添加到分享内容的 intent。这些注释应说明内容中的主要部分或主题。下面的代码示例显示了如何向 intent 添加注释:

ArrayList<String> annotations = new ArrayList<>();

annotations.add("topic1");
annotations.add("topic2");
annotations.add("topic3");

intent.putStringArrayListExtra(
    Intent.EXTRA_CONTENT_ANNOTATIONS,
    annotations
);

智能文本选择

在兼容设备上,Android 8.0 让应用可以帮助用户以更有意义的方式与文本交互。当用户长按某个实体中可识别格式的单词(例如某个地址或餐馆名称)时,系统会选中整个实体。用户会看到一个浮动工具栏,该工具栏包含可以处理所选文本实体的应用。例如,如果系统识别出某个地址,它可以将用户导向地图应用。

系统识别的实体包括地址、网址、电话号码和电子邮件地址。如需了解详细信息,请参阅 TextClassifier。

无障碍功能

ndroid 8.0 支持开发者使用以下无障碍功能创建自己的无障碍服务。如需了解有关如何让您的应用更便于访问的更多信息,请参阅无障碍功能。

无障碍功能按钮

您的无障碍服务现在可以请求在系统的导航区域显示无障碍功能按钮,该按钮让用户可从其设备上的任意位置快速激活您的服务功能。要执行此操作,请在某个 AccessibilityServiceInfo 对象的 android:accessibilityFlags 属性中添加 FLAG_REQUEST_ACCESSIBILITY_BUTTON 标志。稍后,您可以使用 registerAccessibilityButtonCallback() 注册回调。

注:此功能仅适用于提供软件渲染导航区域的设备。请始终使用 isAccessibilityButtonAvailable(),并通过实现 onAvailabilityChanged() 根据无障碍功能按钮的可用性来响应变更。通过该方式,用户可以始终访问您的服务功能,即使该无障碍功能按钮不受支持或变得不可用。

独立的音量调整

Android 8.0 引入了 STREAM_ACCESSIBILITY 音量类别,允许您单独控制无障碍服务音频输出的音量,而不会影响设备上的其他声音。

要使用这个新的流类型来控制无障碍服务音量,请在无障碍服务中设置 FLAG_ENABLE_ACCESSIBILITY_VOLUME 选项。然后,您可以使用 adjustStreamVolume() 更改设备的无障碍服务音频音量。

指纹手势

您的无障碍服务也可以响应替代的输入机制,即沿设备的指纹传感器按特定方向滑动(上、下、左和右)。要接收有关这些交互的回调,请完成以下一系列步骤:
- 声明 USE_FINGERPRINT 权限和 CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 功能。
- 在 android:accessibilityFlags 属性中设置 FLAG_REQUEST_FINGERPRINT_GESTURES 标志。
- 使用 registerFingerprintGestureCallback() 注册回调。

请记住,并非所有设备都包含指纹传感器。您可以使用 isHardwareDetected() 函数识别设备是否支持此传感器。即使对于包含指纹传感器的设备,您的服务也只有在指纹传感器不用于身份验证目的时才可使用它。要识别此传感器何时可用,请调用 isGestureDetectionAvailable() 函数并实现 onGestureDetectionAvailabilityChanged() 回调。

字词级突出显示

要确定 TextView 对象中可见字符的位置,您可以在 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY 中将其作为第一个参数传递到 refreshWithExtraData() 中。随后会更新您为 refreshWithExtraData() 提供的作为第二个参数的 Bundle 对象,使之包含一个可打包的 Rect 对象数组。每个 Rect 对象代表某个特定字符的边界框。

如果您的服务使用 TextToSpeech 对象朗读屏幕上出现的内容,您可以获取有关文本到语音转换引擎何时开始朗读单个合成字词时的准确时间信息,前提是文本到语音转换引擎提供此信息。当引擎即将开始播放特定范围文本的音频时,Text-to-Speech API 会通知您的服务,将使用 onRangeStart() 函数开始朗读此范围的文本。

如果您创建自己的 TextToSpeechService 实现,您可以使用 rangeStart() 函数支持这一新功能。

标准化单端范围值

AccessibilityNodeInfo 的一些实例使用 AccessibilityNodeInfo.RangeInfo 的某个实例来表明界面元素可接受一定范围的值。使用 RangeInfo.obtain() 创建范围或使用 getMin() 和 getMax() 检索此范围的极值时,请注意,Android 8.0 规定了标准化单端范围:

  • 对于没有最小值的范围,Float.NEGATIVE_INFINITY 表示最小值。
  • 对于没有最大值的范围,Float.POSITIVE_INFINITY 表示最大值。

提示文本

Android 8.0 包含可用于与文本可编辑对象的提示文本进行交互的多个函数:

  • isShowingHintText() 和 setShowingHintText() 函数分别显示和设置节点的当前文本内容是否表示节点的提示文本。如果节点不包含可编辑文本,则它不应包含提示文本。
  • 要访问提示文本本身,请使用 getHintText()。即使某个对象当前未显示提示文本,系统也能成功调用 getHintText()。

连续的手势分派

您的服务现在可以使用 GestureDescription.StrokeDescription 构造函数中的最后一个参数 willContinue,指定属于同一设定手势的笔划的顺序。

安全性与隐私

权限

Android 8.0 引入了多个与电话有关的新权限:

  • ANSWER_PHONE_CALLS 允许您的应用通过编程方式接听呼入电话。要在您的应用中处理呼入电话,您可以使用 acceptRingingCall() 函数。
  • READ_PHONE_NUMBERS 权限允许您的应用读取设备中存储的电话号码。

这些权限均被划分为危险类别,属于 PHONE 权限组。

新的帐号访问和 Discovery API

Android 8.0 对应用访问用户帐号的方式引入多项改进。对于由身份验证器管理的帐号,身份验证器在决定对应用隐藏帐号还是显示帐号时可以使用自己的策略。Android 系统跟踪可以访问特定帐号的应用。

在以前的 Android 版本中,想要跟踪用户帐号列表的应用必须获取有关所有帐号的更新,包括具有不相关类型的帐号。Android 8.0 添加了 addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean, java.lang.String[]) 函数,其允许应用指定应接收帐号变更的帐号类型列表。

API 变更

AccountManager 提供六个新函数以帮助身份验证器管理哪些应用可以查看某个帐号:
- setAccountVisibility(android.accounts.Account, java.lang.String, int):针对特定用户帐号和软件包组合设置可见性级别。
- getAccountVisibility(android.accounts.Account, java.lang.String):获取特定用户帐号和软件包组合的可见性级别。
- getAccountsAndVisibilityForPackage(java.lang.String, java.lang.String):允许身份验证器获取帐号和给定软件包的可见性级别。
- getPackagesAndVisibilityForAccount(android.accounts.Account):允许身份验证器获取存储的给定帐号的可见性值。
- addAccountExplicitly(android.accounts.Account, java.lang.String, android.os.Bundle, java.util.Map

Google Safe Browsing API

WebView 类现在添加了一个 Safe Browsing API 来增强网络浏览的安全性。如需了解详细信息,请参阅 Google Safe Browsing API

测试

仪器测试

Android 8.0 为应用的仪器测试提供以下几项额外支持。

针对非默认应用进程运行

现在,您可以指定针对您的应用的默认进程以外的进程运行特定仪器测试。如果您的应用包含多个在不同进程中运行的操作组件,此配置非常有用。

要定义非默认进程仪器测试,请导航至您的清单文件,然后导航至所需的 元素。添加 android:targetProcess 属性,并将它的值设置为以下值之一:
- 特定进程的名称。
- 以逗号分隔的进程名称列表。
- 通配符(”*”),允许针对任何执行 android:targetPackage 属性中指定的软件包中的代码的已启动进程运行仪器测试。

在执行仪器测试时,您可以通过调用 getProcessName() 检查正在测试哪个进程。

在测试过程中报告结果

现在,通过调用 addResults(),您可以在执行仪器测试时(而不用等到测试后)报告结果。

用于测试的模拟 Intent

为了更轻松地为您应用的操作组件创建隔离、独立的界面测试,Android 8.0 引入了 onStartActivity() 函数。要处理您的测试类调用的特定 intent,您可以在 Instrumentation.ActivityMonitor 类的自定义子类中替换此函数。

当您的测试类调用 intent 时,该函数将返回一个存根 Instrumentation.ActivityResult 对象,而不是执行 intent 本身。通过在您的测试中使用这种模拟 intent 逻辑,您可以侧重于自己的操作组件如何准备和处理您传递到不同操作组件或完全不同的应用中的 intent。

行时和工具

平台优化

Android 8.0 为平台引入了运行时优化和其他优化,这些优化将带来多项性能改进。这些优化包括并发压缩垃圾回收、更有效的内存利用和代码区域。

它们可以加快启动时间,并为 OS 和应用带来更好的性能。

更新的 Java 支持

Android 8.0 添加了对更多 OpenJDK Java API 的支持:

  • OpenJDK 8 中的 java.time。
  • OpenJDK 7 中的 java.nio.file 和 java.lang.invoke。
    要详细了解这些新添加的软件包中的类和函数,请参阅 API 参考文档。

如果您想要在 Android Studio 中使用 Java 8 语言功能,您应下载最新的预览版本

更新的 ICU4J Android Framework API

Android 8.0 扩展了 ICU4J Android 框架 API—,它是 ICU4J API 的子集—,供应用开发者在 android.icu 软件包中使用。这些 API 使用设备上具有的本地化数据。因此,您无需在 APK 中编译 ICU4J 库,从而减少 APK 占用空间。

表 1. Android 中使用的 ICU、CLDR 和 Unicode 版本。

Android API 级别 ICU 版本 CLDR 版本 Unicode 版本
Android 7.0(API 级别 24) Android 7.1(API 级别 25) 56 28
Android 8.0 58.2 30.0.3 9.0

Android 企业版

已为运行 Android 8.0 的设备引入新的企业功能和 API。重要功能包括如下:

  • 完全托管的设备中的工作资料使企业可以在管理工作数据与个人数据的同时,将它们分离开来。
  • API 委派允许设备所有者和个人资料所有者将应用管理分配给其他应用。
  • 配置流程中的用户体验改进措施(包含新的自定义选项)缩短了设置时间。
  • 蓝牙、WLAN、备份和安全性方面的新增控制选项使企业可以更精细地管理设备。网络操作组件日志记录可帮助企业追查问题。
    如需详细了解上述及其他新增 Android 企业版 API 和功能,请参阅企业中的 Android

google官网原文链接

如果你想第一时间看我的后期文章,扫码关注公众号,长期推送Android开发文章、最新动态、开源项目,让你各种涨姿势。

      Android开发666 - 安卓开发技术分享
             扫描二维码加关注

Android开发666

作者:lowprofile_coding 发表于2017/9/25 12:50:10 原文链接
阅读:47 评论:0 查看评论

kotlin学习笔记——操作符

$
0
0

这是之前看到别人整理的一篇,觉得挺全面放进笔记里了,不过原文出处记不起来了。。。。

Kotlin学习笔记系列:http://blog.csdn.net/column/details/16696.html


一元操作符

操作符 函数
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a++ a.inc()
a– a.dec()

二元操作符

操作符 函数
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.mod(b)
a..b a.rangeTo(b)
a in b a.contains(b)
a !in b !a.contains(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)

数组操作符

操作符 函数
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, …, i_n] a.get(i_1, …, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, …, i_n] = b a.set(i_1, …, i_n, b)

等于操作符

操作符 函数
a == b a?.equals(b) ?: b === null
a != b !(a?.equals(b) ?: b === null)
相等操作符有一点不同,为了达到正确合适的相等检查做了更复杂的转换,因为要得到一个确切的函数结构比较,不仅仅是指定的名称。方法必须要如下准确地被实现: 
operator fun equals(other: Any?): Boolean 
操作符===和!==用来做身份检查(它们分别是Java中的==和!=),并且它们不能被重载。

函数调用

方法 调用
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, …, i_n) a.invoke(i_1, …, i_n)

作者:chzphoenix 发表于2017/9/26 14:00:21 原文链接
阅读:47 评论:0 查看评论

kotlin学习笔记——重载操作符

$
0
0

Kotlin学习笔记系列:http://blog.csdn.net/column/details/16696.html


Kotlin中有很多操作符可以使用,具体可以见http://blog.csdn.net/chzphoenix/article/details/78094523 

Kotlin的一个特点就是可以重载这些操作符,为操作符赋予不同的行为。


1、重载操作符
在类中实现操作符对应的方法,如:
data class Book(var name: String, var sections: List<String>){
     operator fun get(position: Int): String = sections[position]
}
我们为book这个类实现了get方法,这样就可以直接使用[]操作符简化代码,如:
sectionTitle.text = book1[2]
代替了
sectionTitle.text = book1.sections[2]


2、扩展函数操作符
同样可以使用扩展函数来重载操作符,如:
data class Book(var name: String, var sections: List<String>)

operator fun Book.get(position: Int): String = sections(position)
var title1 = book1[1]


3、invoke
调用invoke时方法可以被省略,所以如果
class ClickAction{
     operator fun invoke(...){
          ...
     }
}
可以直接:
clickAction(...)
注意clickAction是类的一个对象

作者:chzphoenix 发表于2017/9/26 14:05:18 原文链接
阅读:48 评论:0 查看评论

MPAndroidChart项目实战(九)——自定义带文字分段堆积柱状图

$
0
0

一丶效果展示


二丶需求分析及技术点

1.显示每个季度产业(收入)占比,低于5%不显示

与上篇类似,将View换成textView即可,高度小于5%不显示

2.产业颜色与下面显示显色一致,且严格按照设计图颜色

使用map,键值对,一个产业名对应一个颜色

3.柱状图可滑动

同上篇博客

http://blog.csdn.net/dt235201314/article/details/77534468

4.带有动画效果

用白色View遮盖,由下往上收起,形成动画效果

5.图解


三丶看代码

xml

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:text="自定义带文字分段堆积柱状图:" />

<include layout="@layout/item_text_bar" />

<Button
    android:id="@+id/bt_refresh2"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_margin="10dp"
    android:gravity="center"
    android:text="刷新" />
item_text_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp">

        <LinearLayout
            android:id="@+id/bg_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="vertical">

            <LinearLayout
                android:id="@+id/item0"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="12dp">

                <TextView
                    android:id="@+id/tv_num5_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="100%" />

                <View
                    android:id="@+id/left_base_line_text"
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="20dp"
                    android:background="@drawable/view_dash_line" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="12dp">

                <TextView
                    android:id="@+id/tv_num4_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text=" 80%" />

                <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="20dp"
                    android:background="@drawable/view_dash_line" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="12dp">

                <TextView
                    android:id="@+id/tv_num3_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text=" 60%" />

                <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="20dp"
                    android:background="@drawable/view_dash_line" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="12dp">

                <TextView
                    android:id="@+id/tv_num2_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text=" 40%" />

                <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="20dp"
                    android:background="@drawable/view_dash_line" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="12dp">

                <TextView
                    android:id="@+id/tv_num1_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text=" 20%" />

                <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="20dp"
                    android:background="@drawable/view_dash_line" />
            </LinearLayout>

            <View
                android:id="@+id/base_line_text"
                android:layout_width="match_parent"
                android:layout_height="0.5dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="#E6E6E6" />
        </LinearLayout>

        <HorizontalScrollView
            android:id="@+id/bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="70dp"
            android:scrollbars="none">

            <com.barchart.mpchartdemo.view.TextBarGroupView
                android:id="@+id/bar_group_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </HorizontalScrollView>
    </RelativeLayout>

    <com.nex3z.flowlayout.FlowLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/container2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:childSpacing="auto"
        app:childSpacingForLastRow="align"
        android:padding="10dp"
        app:rowSpacing="8dp" />

</LinearLayout>
与上篇背景布局类似,HorizontalScrollView控制左右滑动,动态添加柱状图,以及FlowLayout(自适应布局显示下标)

说一说FlowLayout

bulid添加依赖便可以使用

compile 'com.nex3z:flow-layout:0.1.4'
属性讲解参看文章:

Android第三方库——FlowLayout

之前的文章:

Android删除添加标签(FlowLayout案例)

自定义ViewGroup——TextBarGroupView.java

public class TextBarGroupView extends LinearLayout {
    public TextBarGroupView(Context context) {
        super(context);
        setOrientation(HORIZONTAL);
    }

    public TextBarGroupView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setOrientation(HORIZONTAL);
    }

    public TextBarGroupView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(HORIZONTAL);
    }


    String other = "其它";

    public void init(final List<TextBarDataEntity.Record> datas, final int barHeight, FlowLayout sourceContainer) {
        removeAllViews();
        if (datas == null || datas.isEmpty()) {
            return;
        }
        List<Integer> colors = new ArrayList<>();
        colors.add(Color.parseColor("#3fa0ff"));
        colors.add(Color.parseColor("#98b3e5"));
        colors.add(Color.parseColor("#d7546d"));
        colors.add(Color.parseColor("#51d4c4"));
        colors.add(Color.parseColor("#6d43cc"));
        colors.add(Color.parseColor("#ffb256"));
        colors.add(Color.parseColor("#69390e"));
        colors.add(Color.parseColor("#7ab024"));
        colors.add(Color.parseColor("#a7d0c8"));
        colors.add(Color.parseColor("#a29258"));
        colors.add(Color.parseColor("#297350"));
        colors.add(Color.parseColor("#eebdc7"));
        colors.add(Color.parseColor("#bb59d0"));

        List<TextBarDataEntity.Record.Source> allSourceList = new ArrayList<>();
        List<String> allSourceNameList = new ArrayList<>();
        Map<String, Integer> nameColorMap = new HashMap<>();
        final int lineHeight = (int) getResources().getDisplayMetrics().density * 1;
        for (int i = 0; i < datas.size(); i++) {
            //加载所有来源,去重复
            List<TextBarDataEntity.Record.Source> sourceList = datas.get(i).getSourceList();
            if (sourceList != null && !sourceList.isEmpty()) {
                int j = 0;
                for (TextBarDataEntity.Record.Source entry : sourceList) {
                    if (!nameColorMap.containsKey(entry.getSourceName())) {
                        Integer colorValue = colors.get(j % colors.size());
                        if (!nameColorMap.containsValue(colorValue)) {
                            nameColorMap.put(entry.getSourceName(), colorValue);
                        } else {
                            int index=colors.indexOf(colorValue);
                            for(int x=index;x<colors.size();x++){
                                Integer colorValue1 = colors.get(x % colors.size());
                                if(!nameColorMap.containsValue(colorValue1)){
                                    nameColorMap.put(entry.getSourceName(), colorValue1);
                                    break;
                                }
                            }
                        }
                    }
                    if (!allSourceNameList.contains(entry.getSourceName())) {
                        allSourceNameList.add(entry.getSourceName());
                        allSourceList.add(entry);
                    }
                    j++;
                }
                Collections.reverse(sourceList);
            }
        }
        initAllSourceLayout(allSourceList, sourceContainer, nameColorMap);
        for (int i = 0; i < datas.size(); i++) {
            final View item = LayoutInflater.from(getContext()).inflate(R.layout.text_source_item_group, this, false);
            final TextBarView barView = (TextBarView) item.findViewById(R.id.barview);
            barView.init(datas.get(i), barHeight + lineHeight, nameColorMap);
            ((TextView) item.findViewById(R.id.time)).setText(datas.get(i).getTimeScale());
            item.findViewById(R.id.time).setMinimumWidth((int) (getResources().getDisplayMetrics().density*80));
            if (i == 0) {
                final LayoutParams lp = (LayoutParams) item.getLayoutParams();
                lp.leftMargin = 0;
                addView(item, lp);
            } else {
                addView(item);
            }
            final View coverView = item.findViewById(R.id.cover);
            ViewGroup.LayoutParams lp = coverView.getLayoutParams();
            lp.height = barHeight;
            coverView.setLayoutParams(lp);
            //动画
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    final int initCoverHeight = coverView.getHeight();
                    ObjectAnimator anim = ObjectAnimator.ofFloat(coverView, "translationY", 0, -initCoverHeight);
                    anim.setDuration(1000);
                    anim.start();
                }
            }, (i + 1) * 500);
        }

        getViewTreeObserver().

                addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(this);
                        HorizontalScrollView.LayoutParams lp = (HorizontalScrollView.LayoutParams) getLayoutParams();
                        lp.topMargin = barHeight / 5 / 2 - lineHeight;
                        setLayoutParams(lp);
                        return false;
                    }
                });

    }

    private void initAllSourceLayout(List<TextBarDataEntity.Record.Source> list, FlowLayout sourceContainer, Map<String, Integer> nameColorMap) {
        sourceContainer.removeAllViews();
        for (TextBarDataEntity.Record.Source source : list) {
            View item = LayoutInflater.from(getContext()).inflate(R.layout.pie_lable_item, sourceContainer, false);
            GradientDrawable bg = (GradientDrawable) item.findViewById(R.id.icon).getBackground();
            TextView txt = (TextView) item.findViewById(R.id.txt);
            bg.setColor(nameColorMap.get(source.getSourceName()));
            item.findViewById(R.id.icon).setBackground(bg);
            txt.setText(source.getSourceName());
            sourceContainer.addView(item);
        }
    }

}

看init()方法

第一个参数 List<TextBarDataEntity.Record> datas 为实体类展示数据

造数据核心代码:

public List<Record>  parseData(){
    recordList = new ArrayList<>();
    Random r = new Random();
    for (int i= 0;i<=4;i++){
        Record record = new Record();
        record.setTimeScale("第" + (i+1) + "周");
        List<Record.Source> list = new ArrayList<>();
        for (int j=0; j<= 3; j++ ){
            Record.Source source = new Record.Source();
            source.setSourceName("TCL第" + (j+1) + "产业");
            source.setSourceNum(r.nextInt(10*(j+1)));
            list.add(source);
        }
        Record.Source source = new Record.Source();
        source.setSourceName("TCL第5产业");
        int sourceNum = 100 - list.get(0).getSourceNum() - list.get(1).getSourceNum()
                - list.get(2).getSourceNum() - list.get(3).getSourceNum();
        source.setSourceNum(sourceNum);
        list.add(source);
        record.setSourceList(list);
        recordList.add(record);
    }
    return recordList;
}
随机数10,20,30,40以内,最后100前去前面的数,刚好100,除以100为占比数


第二个参数 int barHeight 高度,这里为柱状图高度,5倍上面的那个横线布局 id item0 高度


第三个参数 FlowLayout sourceContainer,自定义view容器,动态添加子view,“第1产业”


看for循环(是不是很牛逼的感觉,cnm产品要求的)

运用map,一个产业名对应一个颜色数值

注意了,为什么后面要循环遍历,因为各个季度产业数可能不同,例:季度一(1 2 3 4产业)季度二(3 4 5 6产业)

Collections.reverse(sourceList); 这一句,逆序展示业务要求(后台排好大小顺序)


initAllSourceLayout()方法

第一个参数 List<TextBarDataEntity.Record.Source> list 产业名称

第二个参数  FlowLayout sourceContainer 容器  pie_lable_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <View
        android:id="@+id/icon"
        android:layout_width="10dp"
        android:layout_height="10dp"
        android:background="@drawable/source_small" />

    <TextView
        android:id="@+id/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="12sp" />
</LinearLayout>
第三个参数 Map<String, Integer> nameColorMap 产业名→颜色

看代码,这个方法就是添加效果图下面的,产业1 2 3...了


第二个for循环(添加自定义View带文字柱状图 + 动画实现)

text_source_item_group.xml(这个带文字柱状的布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/text_bar_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/time"
        android:layout_alignRight="@+id/time"
        android:paddingLeft="10dp"
        android:paddingRight="10dp">

        <com.barchart.mpchartdemo.view.TextBarView
            android:id="@+id/barview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"></com.barchart.mpchartdemo.view.TextBarView>

        <View
            android:id="@+id/cover"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white"
            android:visibility="visible" />
    </FrameLayout>

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_bar_container"
        android:layout_marginTop="15dp"
        android:gravity="center"
        android:text="17.01.01-17.03.31"
        android:textSize="12sp" />
</RelativeLayout>
这里FrameLayout,用一个白色View遮住TextBarView收起形成动画

TextBarView.java

public class TextBarView extends LinearLayout {
    public TextBarView(Context context) {
        super(context);
        setOrientation(VERTICAL);
    }

    public TextBarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setOrientation(VERTICAL);
    }

    public TextBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(VERTICAL);
    }

    DecimalFormat format = new DecimalFormat("##.##");
    PopupWindow popupWindow;
    View popView;

    public void init(TextBarDataEntity.Record record, int height, Map<String, Integer> nameColorMap) {
        if (record.getSourceList() == null && record.getSourceList().isEmpty()) {
            return;
        }
        popView = LayoutInflater.from(getContext()).inflate(
                R.layout.pop_bg, null);
        //计算空白填充部分占比
        double blankScale = 1;
        for (int i = 0; i < record.getSourceList().size(); i++) {
            blankScale -= record.getSourceList().get(i).getSourceNum();
        }
//        if (blankScale==1) {
//            TextView item = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.negative_sentiment_source_item_txt, this, false);
//            ViewGroup.LayoutParams lp = item.getLayoutParams();
//            lp.height = (int) (blankScale * height);
//            addView(item);
//            return;
//        }

        for (int i = 0; i < record.getSourceList().size(); i++) {
            final TextView item = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.text_source_item_txt, this, false);
            final TextBarDataEntity.Record.Source source = record.getSourceList().get(i);
            item.setText(source.getSourceNum() >= 5 ? format.format(source.getSourceNum()) + "%" : "");
            GradientDrawable bg = (GradientDrawable) getResources().getDrawable(R.drawable.n_s_bar_bg);
            bg.setColor(nameColorMap.get(source.getSourceName()));
            item.setBackground(bg);
            ViewGroup.LayoutParams lp = item.getLayoutParams();
            lp.height = (int) (source.getSourceNum()/(double)100 * height);
            addView(item, lp);
        }
    }

    private void showPop(final View view) {
        if (popupWindow != null)
            popupWindow.dismiss();
        popupWindow = null;
        popupWindow = new PopupWindow(popView,
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, false);
        popupWindow.setBackgroundDrawable(new BitmapDrawable());
        popupWindow.setOutsideTouchable(true);
        popupWindow.showAsDropDown(view, view.getWidth() / 2, view.getHeight() / 2);
    }
}
这里与上一篇的BarView类似,带文字柱状图就没必要再点击弹框了

直接看init()方法的for循环

text_source_item_txt.xml (就是一个textview)

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/gray3"
    android:gravity="right|center_vertical"
    android:paddingRight="5dp"
    android:text="50%"
    android:textColor="@android:color/white"
    android:textSize="12sp" />

item.setText(source.getSourceNum() >= 5 ? format.format(source.getSourceNum()) + "%" : "");
小于5%,不显示

添加bg

lp.height = (int) (source.getSourceNum()/(double)100 * height);
这里表示每个text站总高度的百分比,必须加上double


白色view揭盖动画

获取高度运用ObjectAnimator收起

给自定义带文字布局添加topMargin

getViewTreeObserver().

。。。。

四丶往期文章

MPAndroidChart常见设置属性(一)——应用层 
MPAndroidChart项目实战(一)——实现对比性柱状图 
MPAndroidChart项目实战(二)——双平滑曲线(双折线图)和MarkView实现 
MPAndroidChart项目实战(三)——饼状图实现和文字重合问题解决 
MPAndroidChart项目实战(四)——柱状图实现及X轴文字不显示问题和柱状图上显示文字 
MPAndroidChart X轴文字斜着显示 
MPAndroidChart项目实战(五)——组合图实现趋势图 
MPAndroidChart项目实战(六)——自定义1MPAndroidChart滑动冲突解决(搞不定产品设计师就只能搞自己) 
MPAndroidChart项目实战(七)——自定义横向柱状图 
MPAndroidChart项目实战(八)——自定义分段堆积柱状图 
MPAndroidChart项目实战(九)——自定义带文字分段堆积柱状图 

五丶跪求关注下载源码,200粉小目标
欢迎关注我的博客及微信公众号,后面会给大家带来更多相关MPAndroidChart无法解决的仿MPAndroidChart图标自定义控件
源码下载记得顺便Star哦~
下载链接:https://github.com/JinBoy23520/MPAndroidChartDemoByJin

作者:DT235201314 发表于2017/9/25 15:09:23 原文链接
阅读:8 评论:1 查看评论

linux学习---linux命令大全

$
0
0

一.管理文件和目录

1.1 pwd命令

该命令的英文解释为print working directory(打印工作目录)。输入pwd命令,Linux会输出当前目录。

1.2 cd命令

cd命令用来改变所在目录。

cd /      转到根目录中
cd ~    
转到/home/user用户目录下
cd /usr
转到根目录下的usr目录中-------------绝对路径
cd test
转到当前目录下的test子目录中-------相对路径

1.3 ls命令

ls命令用来查看目录的内容。

1.4 cat命令

cat命令可以用来合并文件,也可以用来在屏幕上显示整个文件的内容。

cat snow.txt 该命令显示文件snow.txt的内容,ctrl+D退出cat

1.5 grep命令

grep命令的最大功能是在一堆文件中查找一个特定的字符串。

grep money test.txt

以上命令在test.txt中查找money这个字符串,grep查找是区分大小写的。

通常此部分可以和管道一起使用ps |grep bash

管道:把一个命令的输出当成文件交给右边的命令处理

1.6 touch命令

touch命令用来创建新文件,他可以创建一个空白的文件,可以在其中添加文本和数据。

touch newfile 该命令创建一个名为newfile的空白文件。

1.7 cp命令

cp命令用来拷贝文件,要复制文件,输入命令:

cp <source filename> <target filename>

cp t.txt Document/t    该命令将把文件t.txt复制到Document目录下,并命名为t

1.8 mv命令

mv命令用来移动文件。

mv t.txt Document    把文件t.txt 移动到目录Document中。

1.9 rm命令

rm命令用来删除文件。

rm t.txt   该命令删除文件t.txt

1.10 rmdir命令

rmdir命令用来删除目录。

补充:

1.11 mkdir命令

mkdir用来创建目录

 

二.有关磁盘空间的命令

2.1 mount命令

mount命令的功能是挂载文件系统,可以挂载硬盘、光盘、软盘,也可以挂载NFS网络文件系统。这个命令的标准用法如下:

mount –t 设备类型存放目录
mount IP
地址:/所提供的目录存放目录

在目录/mnt下,挂上iso9660文件系统。输入命令:

mount –t iso9660 /dev/hdb /cdrom

2.2 umount命令

umount命令的功能是卸载已挂上的文件系统,在关闭系统前应该把所有挂载上的文件系统卸载。这个命令和mount命令是相对的。用法:

umount 已挂上的目录或设备

卸载已挂上的/cdrom目录,输入命令:

umount /cdrom

卸载已挂上的某个分区,输入命令:

umount /dev/hdb1

2.3 df命令

df命令用来检查硬盘分区和已挂在的文件系统的磁盘空间,也就是说,检查硬盘的使用量。标准用法如下:

df [-选项]

例如,要列出全部文件系统和各分区的磁盘使用情况,输入命令:

df –a

2.4 du命令

du命令的功能是用于显示文件目录或大小。标准用法:

du [-选项]

2.5 fsck命令

fsck命令的功能是检查和修复Linux文件系统,这个命令最好在没有人或是没有分区挂上来时使用,其实每次开机系统都会做一次检查,看是否有坏轨或数据流失的现象。用法:

fsck (-选项) 分区名称

三.文件备份和压缩命令

3.1 bzip2命令

要使用bzip2来压缩文件,在shell提示下输入命令:

bzip2 filename

文件即会被压缩,并被保存为filename.bz2

要解压缩文件,输入命令:

bunzip2 filename.bz2

filename.bz2会被删除,而以filename代替。

bzip2 filename.bz2 file1 file2 file3 /usr/work/school

上面的命令把file1file2file3以及/usr/work/school目录中的内容压缩起来放入filename.bz2

3.2 gzip命令

要使用gzip来压缩文件,输入命令:

gzip filename

文件即会被压缩,并被保存为filename.gz

要解压缩文件,输入命令:

gunzip filename.gz

filename.gz会被删除,而以filename代替。

gzip -r filename.gz file1 file2 file3 /usr/work/school

上面的命令把file1file2file3以及/usr/work/school目录中的内容压缩起来放入filename.gz

3.3 zip命令

zip命令的使用方法同gzip

3.4 tar命令

tar命令最早是用来做磁带备份的,但是由于硬盘容量越来越大,因此现在主要用这个命令来备份所有的文件。tar这个命令把大量的文件和目录打包成一个文件。

要创建一个tar文件,输入命令:

tar –cvf filename.tar directory/file /home/mine

上面的命令将directory/file/home/mine放入归档文件中。

要列出tar文件的内容,输入命令:

tar –tvf filename.tar

要抽取tar文件的命令,输入命令:

tar –xvf filename.tar

这个命令不会删除tar文件,但会把解除归档的内容复制到当前工作目录下,并保留归档文件所使用的任何目录结构。

请记住,tar默认不压缩文件。要创建一个使用tarbzip2来归档压缩的文件,使用-j选项:

tar –cjvf filename.tbz file

如果使用bunzip2命令解压filename.tbz文件,则filename.tbz会被删除,以filename.tar代替。

要扩展并解除归档bzip tar文件,输入命令:

tar –xjvf filename.tbz

要创建一个用targzip归档并压缩的文件,使用-z选项:

tar –czvf filename.tgz file

如果使用gunzip命令解压filename.tgz文件,则filename.tgz会被删除,以filename.tar代替。

四.有关关机和查看系统信息的命令

4.1 shutdown命令

要使用这个命令必须保证是根用户,否则使用su命令改变为根用户。命令格式如下:

shutdown –(选项)

加入要在2min内关机,输入命令:

shutdown –t 2

如果是关机后重启,输入命令:

shutdown –r

4.2 reboot命令

这个命令也是一个关机命令,只有输入,不加任何参数,系统会以最快的速度关机,且不将内存或缓冲区里的东西写回硬盘。

4.3 ps命令

ps命令用来查看在计算机系统中有哪些程序正在执行,及其执行的情况。这是一个相当强大的命令,可以用它来找出所有的process id和名称。另外,ps命令也可以用来列出所有程序占用内存的情况。用法如下:

ps –(选项)

详细参考:http://blog.csdn.net/xiaoxiaopengbo/article/details/78097696

4.4 top命令

top命令可以查看目前程序的执行情景和内存使用。它和ps类似,不过,它会几秒钟更新一次系统状态,方便追踪。要离开这个程序,按Ctrl+C键就可以了。

4.5 kill命令

kill命令用来终止一个正在执行中的进程。如果一个程序执行过程中失败了,可以把这个程序终止,避免留在内存中占用系统资源。不过,它的实际意义是送一个信号给这个正在执行的程序,叫它自杀。可以送很多信号给这些程序,也可以让他们受到信号后做很多事情。标准用法:

kill –(选项) pid

在执行kill命令前。可以先用ps命令查一下某宕掉程序的pid,然后使用kill除去某个程序。例如,终止pid90的程序:

kill 90

pid 323 的行程砍掉 (kill)

kill -9 323

pid 456 的行程重跑(restart)

kill -HUP 456

4.6 date命令

date命令用来显示、设定和修改现在的时间和日期。标准用法:

date –(选项) 显示时间格式(+号开头,后加格式)
date
设定时间格式

如果输入命令:

date “+%x,%r”

系统返回如下信息:

2010326日,下午 180649

4.7 cal命令

cal命令有两种功能:显示月历以及年历。

直接输入cal命令则系统会显示目前月份的月历。

若要显示一整年的年历,可以在cal命令后加4位数的公元年份。例如要显示2008年的年历,必须输入:

cal 2008

若输入 cal 08 ,则最显示公元8年的年历。

若只需要查看某一年份中某一月份的月历,可以输入:cal 月份公元年份。例如输入:"cal 12 2004”

五.管理使用者和设立权限的命令

5.1 chmod命令

chmod命令用来改变许可权限。读取、写入和执行是许可权限中的三个主要设置。因为用户在他们的账号被创建时就被编入一个组群,所以还可以指定那些组群可以读取、写入或执行某一文件。其中:

r—文件可以被读取
w—
文件可以被写入
x—
文件可以被执行,如果文件是程序的话

可以使用带有-lls命令来仔细查看一个文件的许多细节。

chmod命令用来设定文件的权限。标准用法:

chmod 文件的使用者(u,g,o,a)增减(+,-,=)权限名称(r,w,x) 文件

删除某一文件的所有权限,输入命令:

chmod a-rwx test.txt

为文件所有者添加权限,输入命令:

chmod u+rwx test

还可以用数字表示权限:4——读取,2——写入,1——执行。下面的两个命令等价:

chmod 751 filename
chmod u+rwx,g=rx,0=x filename

5.2 su命令

su命令用来修改用户。这个命令非常重要,它可以让一个普通的使用者拥有超级用户或其他使用者的权限。不过,这个命令必须具有超级用户或其他使用者的口令才能成为超级用户或其他使用者。如果要离开,可以输入exit。标准用法:

su 用户名  (如果没有输入用户名则预设为root)

举例说明,假设当前用户user01,想要转变为user02,则输入命令:

su user02

系统返回:

password

此时,输入user02的指令,就会变为user02

5.3 useradd命令

useradd命令用来增加用户,只有根用户才能增加用户。如果没有登录为根用户,输入su,再输入根口令即可。

要增加用户,输入命令:

useradd 用户名

然后,根据提示为新用户输入一个口令即可。

六.线上查询的命令

6.1 man命令

man命令用来查询和解释一个命令的使用方法和这个命令的注意事项。这个查询查询在每个Linux上都有。通常,使用者只要输入命令man和这个命令的名称shell就会列出一份完整的说明。标准用法:

man 命令的名称

要查询ls命令的说明书页,输入命令:

man ls

要翻阅说明书页,可以使用Page UpPage Down键,或使用空格键向后翻一页,使用b向前翻。要退出说明书页,输入命令q。要在说明书页中搜索关键字,输入命令/和要搜索的关键字或短语,然后按Enter键即可。所有出现在说明书页中的关键字都会被突出显示,允许快速地阅读上下文中的关键字。

6.2 locate命令

locate命令的主要功能是定位文件和目录。有时候,只知道某一文件或目录存在,却不知道在哪儿,就可以用locate来定位文件和目录。使用locate命令,将会看到每一个包括搜索田间的文件和目录。例如,如果想要搜索带有test的这个词的文件,输入命令:

locate test

locate命令使用数据库来定位带有test这个词的文件或目录。

6.3 whatis命令

whatis命令用来查询某个命令的含义。用法简单,也不需要什么参数,直接在whatis命令后加上所要查询的命令就可以了,但是却很实用。

要查询mv命令的含义,输入命令:

whatis mv

七.文件阅读的命令

7.1 head命令

head命令可以用来查看文件的开头部分。此命令的格式是:

head 文件名

默认设置,它只查看文件的前10行。但可以通过指定一个数字选项来改变要显示的行数,命令如下:

head –20 文件名

这个命令将会查看文件的前20行。

7.2 tail命令

tail命令和head命令恰恰相反。使用tail命令,可以查看文件结尾的10行。这有助于查看日志文件的最后10行来阅读重要的系统信息。还可以使用tail观察日志文件更新的过程。使用-f选项,tail会自动实时地把打开文件中的新信息显示到屏幕上。例如,要活跃地观察/var/log/messages,以根用户身份在shell下输入以下命令:

tail –f /var/log/messages

7.3 less命令

less命令与more命令相似。

7.4 more命令

moreless的主要区别是,less允许使用箭头来前后移动,而more使用空格键和b键来前后移动。使用lsmore来列举/etc目录下的内容:

ls –al /etc | more

要使用more在文本文件中搜索关键字,按/键并输入命令搜索条目:

/foo

使用空格键来先前翻阅页码。按q键退出。

more命令标准格式为:

more [选项] [fileNames]

例如:

more -s testfile 逐页显示testfile之档案内容,如有连续两行以上空白行则以一行空白行显示。 
more +20 testfile
从第20行开始显示testfile之档案内容。

八.网络操作命令

8.1 ftp命令

ftp命令用来传输文件,非常重要。如果在网络上看到一个很重要的文件,就可以用这个命令把那个文件传到自己的机器上来。

标准用法:

ftp (-选项) 主机名称或IP地址

举例说明,用ftp登录ftp.dark.com主机,输入命令:

ftp ftp.dark.com

注意:用户必须有相应的存取权限,否则不能从远程系统中获得文件或向远程系统中传输文件。为了使用ftp来传输文件,用户必须知道远程计算机上的合法用户名和口令。

8.2 bye命令

ftp模式下,输入bye即可中断目前的连线作业,并结束ftp的执行,没有选项。

8.3 ping命令

执行ping命令,它会使用ICMP传输协议,发出要求回应的信息,若远程主机的网络没有什么问题,就会回应信息,因而得知该主机运作正常。

标准用法:

ping (-选项) 主机名称或IP地址

举例说明,检测des.bllod.net主机网络功能是否正常,送出去要去信息需完成5次回应,每次间隔10s,数据包的大小为512B,输入命令:

ping –c 5 –i 10 –s 504 –t 64 des.blood.net

8.4 telnet命令

telnet命令的主要功能是进行远程登录。该命令允许用户在使用telnet协议的远程计算机之间进行通信,用户可以通过网络在远程计算机上登录,就像登录到本地机上执行命令一样。为了通过telnet登录计算机,必须知道远程计算机上的合法用户名和口令。telnet只为普通终端提供终端仿真,而不支持X Window等图形环境。

标准用法:

telnet 主机名或IP

例如登录IP地址为140.114.63.12的计算机,输入命令:

telnet 140.114.63.12

一旦telnet成功连接到远程系统上,就显示登录信息,并提示用户输入用户名和口令。如果用户名和口令正确,就能成功登录并在远程系统上工作。用户结束了远程会话后,一定要确保使用logout命令退出远程系统。然后telnet报告远程会话被关闭,并返回到用户本地机的shell提示符下。

8.5 rlogin命令

rlogin也是用来远程登录的。它的英文含义是remote login。该命令与telnet命令很相似,允许用户启动远程系统上的交互命令会话。用法:

rlogin (-选项) host

例如,要登入别人的计算机。输入命令:

rlogin –l inin 140.114.125.24

8.6 netstat命令

netstat命令的主要功能是了解Linux系统的网络情况。假设没有指定任何参数给netstat命令,则效果和指定-F参数相同。

用法:

netstat (-选项)

九.定位、查找文件的命令

which

语法

which command

说明

依序从path环境变量所列的目录中找出command的位置,并显示完整路径的名称。在找到第一个符合条件的程序文件时,就立刻停止搜索,省略其余未搜索目录。

范例,找出ls命令的程序文件的位置:

which ls

系统输出:

/usr/bin/ls

whereis

语法

whereis [option] name

说明

找出特定程序的可执行文件、源代码文件以及manpage的路径。你所提供的name会被先除去前置的路径以及任何.ext形式的扩展名。

whereis 只会在标准的Linux目录中进行搜索。

常用选项

-b

只搜索可执行文件。

-m

只搜索manpage

-s

只搜索源代码文件。

-B directory

更改或限定搜索可执行的文件的目录。

-M directory

更改或限定搜索manpage的目录。

-S directory

更改或限定搜索源代码文件的目录。

find

语法

find paths expression [action]

说明

paths为搜索起点逐层往下找出每一个符合expression条件的文件,并对该文件执行action所代表的动作。expression是搜索条件,它由一个代表匹配项目的选项以及一个代表匹配模式的参数构成。

action是处理动作,它有一个代表处理方式的选项以及一个操作参数构成。若不指定action,则默认动作是显示出文件名。

常用的搜索条件

-name pattern
-path pattern
-lname pattern

找出名称、路径名称或符号链接的目标匹配pattern模式的文件。pattern可以包含shell的文件名通配符,路径是相对于搜索起点的。

常见处理动作

-print

显示出文件的相对路径(相对于搜索起点)。

-exec cmd /;

执行指定的shell命令。若cmd含有任何shell特殊字符,则他们之前都必须加上/符号,以免shell立刻执行他们。在cmd里,可以用”{}”符号(包括双引号)表示find所找出的文件。

locate

语法

locate patterns

说明

第一次执行locate时,它会建立一个索引数据库,当往后再次执行时,它便从索引数据库中迅速找出文件的位置。locate很适合用来反复搜索很少变动的目录树,但是对于刚改名的旧文件以及新建的文件,locate就找不到了,除非重建数据库

updatedb

语法

updatedb [option]

说明

更新slocate的索引数据库。

选项

-e directories

略过directories所列的目录。

十.其他命令


9.1 echo命令

echo命令用来在显示器上输出一段文字,这个命令常用来输出一些提示信息,因此这个命令的意义在于输出一些文字。它的用法也很简单:

echo –(参数) 字串 (可以用"”,也可以不用,显示略有区别)

参数n代表输出文字后不换行,如果不加参数会自动换行。

输入命令:

echo “welcome   to   use   Linux”

输出结果为:

welcome   to   use   Linux

如果不加"”,则输出结果为:

welcome to use Linux

它们的区别在于后一个输出,每两个输出之间只隔一个空格,这是因为echo把这些输出看做字串的缘故。

9.2 clear命令

clear命令的主要功能是清除显示器,这个命令很简单,只要输入clear即可。

9.3 passwd命令

passwd命令用来修改用户的密码。

shell下输入

passwd

然后,根据提示输入旧密码和新密码即可。

9.4 lpr命令

lpr命令的功能是把指定的文件发送到打印队列中。例如,lpr foo.txt 会打印 foo.txt 文件。

标准用法:

lpr filename

要查看在打印队列中等待的作业,在命令行中输入命令lpq。系统返回如下信息:

active root 389 foo.txt

在这个例子中,389是作业号码。还可以取消打印队列中的作业,格式是:

lprm作业号码

输入命令:

lprm 389

这样就去修了 foo.txt打印作业。

9.5 exit命令

退出当前的shell,如果是最后一个shell,那么会退出终端

十一.VI使用

参照:http://blog.csdn.net/xiaoxiaopengbo/article/details/50839658

十二.GCC使用

http://blog.csdn.net/xiaoxiaopengbo/article/details/50339877

作者:XiaoXiaoPengBo 发表于2017/9/26 16:30:50 原文链接
阅读:0 评论:0 查看评论

Android解析ClassLoader(一)Java中的ClassLoader

$
0
0

相关文章
Java虚拟机系列

前言

热修复和插件化是目前比较热门的技术,要想更好的掌握它们需要了解ClassLoader,因此也就有了本系列的产生,这一篇我们先来学习Java中的ClassLoader。

1.ClassLoader的类型

Java虚拟机(一)结构原理与运行时数据区域这篇文章中,我提到过类加载子系统,它的主要作用就是通过多种类加载器(ClassLoader)来查找和加载Class文件到 Java 虚拟机中。
Java中的类加载器主要有两种类型,系统类加载和自定义类加载器。其中系统类加载器包括3种,分别是Bootstrap ClassLoader、 Extensions ClassLoader和 App ClassLoader。

1.1 Bootstrap ClassLoader

用C/C++代码实现的加载器,用于加载Java虚拟机运行时所需要的系统类,如java.lang.*、java.uti.*等这些系统类,它们默认在$JAVA_HOME/jre/lib目录中,也可以通过启动Java虚拟机时指定-Xbootclasspath选项,来改变Bootstrap ClassLoader的加载目录。
Java虚拟机的启动就是通过 Bootstrap ClassLoader创建一个初始类来完成的。由于Bootstrap ClassLoader是使用C/C++语言实现的, 所以该加载器不能被Java代码访问到。需要注意的是Bootstrap ClassLoader并不继承java.lang.ClassLoader。
我们可以通过如下代码来得出Bootstrap ClassLoader所加载的目录:

public class ClassLoaderTest {
    public static void main(String[]args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
    }
}

打印结果为:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes

可以发现几乎都是$JAVA_HOME/jre/lib目录中的jar包,包括rt.jar、resources.jar和charsets.jar等等。

1.2 Extensions ClassLoader

用于加载 Java 的拓展类 ,拓展类的jar包一般会放在$JAVA_HOME/jre/lib/ext目录下,用来提供除了系统类之外的额外功能。也可以通过-Djava.ext.dirs选项添加和修改Extensions ClassLoader加载的路径。
通过以下代码可以得到Extensions ClassLoader加载目录:

System.out.println(System.getProperty("java.ext.dirs"));

打印结果为:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext

1.3 App ClassLoader

负责加载当前应用程序Classpath目录下的所有jar和Class文件。也可以加载通过-Djava.class.path选项所指定的目录下的jar和Class文件。

1.4 Custom ClassLoader

除了系统提供的类加载器,还可以自定义类加载器,自定义类加载器通过继承java.lang.ClassLoader类的方式来实现自己的类加载器,除了 Bootstrap ClassLoader,Extensions ClassLoader和App ClassLoader也继承了java.lang.ClassLoader类。关于自定义类加载器后面会进行介绍。

2.ClassLoader的继承关系

运行一个Java程序需要用到几种类型的类加载器呢?如下所示。

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = ClassLoaderTest.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);//1
            loader = loader.getParent();
        }
    }
}

首先我们得到当前类ClassLoaderTest的类加载器,并在注释1处打印出来,接着打印出当前类的类加载器的父加载器,直到没有父加载器终止循环。打印结果如下所示。

sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@1b6d3586

第1行说明加载ClassLoaderTest的类加载器是AppClassLoader,第2行说明AppClassLoader的父加载器为ExtClassLoader。至于为何没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是因为Bootstrap ClassLoader是由C/C++编写的,并不是一个Java类,因此我们无法在Java代码中获取它的引用。

我们知道系统所提供的类加载器有3种类型,但是系统提供的ClassLoader相关类却不只3个。另外,AppClassLoader的父类加载器为ExtClassLoader,并不代表AppClassLoader继承自ExtClassLoader,ClassLoader的继承关系如下所示。

可以看到上图中共有5个ClassLoader相关类,下面简单对它们进行介绍:

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher 的内部类,Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

3 双亲委托模式

3.1 双亲委托模式的特点

类加载器查找Class所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。
这样讲可能会有些抽象,来看下面的图。

我们知道类加载子系统用来查找和加载Class文件到 Java 虚拟机中,假设我们要加载一个位于D盘的Class文件,这时系统所提供的类加载器不能满足条件,这时就需要我们自定义类加载器继承自java.lang.ClassLoader,并复写它的findClass方法。加载D盘的Class文件步骤如下:

  1. 自定义类加载器首先从缓存中要查找Class文件是否已经加载,如果已经加载就返回该Class,如果没加载则委托给父加载器也就是App ClassLoader。
  2. 按照上图中红色虚线的方向递归步骤1。
  3. 一直委托到Bootstrap ClassLoader,如果Bootstrap ClassLoader在缓存中还没有查找到Class文件,则在自己的规定路径$JAVA_HOME/jre/libr中或者-Xbootclasspath选项指定路径的jar包中进行查找,如果找到则返回该Class,如果没有则交给子加载器Extensions ClassLoader。
  4. Extensions ClassLoader查找$JAVA_HOME/jre/lib/ext目录下或者-Djava.ext.dirs选项指定目录下的jar包,如果找到就返回,找不到则交给App ClassLoader。
  5. App ClassLoade查找Classpath目录下或者-Djava.ext.dirs选项所指定的目录下的jar包和Class文件,如果找到就返回,找不到交给我们自定义的类加载器,如果还找不到则抛出异常。

总的来说就是Class文件加载到类加载子系统后,先沿着图中红色虚线的方向自下而上进行委托,再沿着黑色虚线的方向自上而下进行查找,整个过程就是先上后下。

类加载的步骤在JDK8的源码中也得到了体现,来查看抽象类的ClassLoader方法,如下所示。

 protected Class<?> More ...loadClass(String name, boolean resolve)
         throws ClassNotFoundException
     {
         synchronized (getClassLoadingLock(name)) {
             Class<?> c = findLoadedClass(name);//1
             if (c == null) {
                 long t0 = System.nanoTime();
                 try {
                     if (parent != null) {
                         c = parent.loadClass(name, false);//2
                     } else {
                         c = findBootstrapClassOrNull(name);//3
                     }
                 } catch (ClassNotFoundException e) {            
                 }
                 if (c == null) {
                     // If still not found, then invoke findClass in order
                     // to find the class.
                     long t1 = System.nanoTime();
                     c = findClass(name);//4
                     // this is the defining class loader; record the stats
                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                     sun.misc.PerfCounter.getFindClasses().increment();
                 }
             }
            if (resolve) {
                 resolveClass(c);
             }
            return c;
         }
     }

注释1处用来检查类是否已经加载,如果已经加载则后面的代码不会执行,最后会返回该类。没有加载则会接着向下执行。
注释2处,如果父类加载器不为null,则调用父类加载器的loadClass方法。如果父类加载器为null则调用注释3处的findBootstrapClassOrNull方法,这个方法内部调用了Native方法findLoadedClass0,findLoadedClass0方法中最终会用Bootstrap Classloader来查找类。如果Bootstrap Classloader仍没有找到该类,也就说明向上委托没有找到该类,则调用注释4处的findClass方法继续向下进行查找。

3.2 双亲委托模式的好处

采取双亲委托模式主要有两点好处:
1. 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改
类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

4.自定义ClassLoader

系统提供的类加载器只能够加载指定目录下的jar包和Class文件,如果想要加载网络上的或者是D盘某一文件中的jar包和Class文件则需要自定义ClassLoader。
实现自定义ClassLoader需要两个步骤:
1. 定义一个自定义ClassLoade并继承抽象类ClassLoader。
2. 复写findClass方法,并在findClass方法中调用defineClass方法。

下面我们就自定义一个ClassLoader用来加载位于D:\lib的Class文件。

4.1 编写测试Class文件

首先编写测试类并生成Class文件,如下所示。

package com.example;
public class Jobs {
    public void say() {
        System.out.println("One more thing");
    }
}

将这个Jobs.java放入到D:\lib中,使用cmd命令进入D:\lib目录中,执行Javac Jobs.java对该java文件进行编译,这时会在D:\lib中生成Jobs.class。

4.2 编写自定义ClassLoader

接下来编写自定义ClassLoader,如下所示。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class DiskClassLoader extends ClassLoader {
    private String path;
    public DiskClassLoader(String path) {
        this.path = path;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        byte[] classData = loadClassData(name);//1
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            clazz= defineClass(name, classData, 0, classData.length);//2
        }
        return clazz;
    }
    private byte[] loadClassData(String name) {
        String fileName = getFileName(name);
        File file = new File(path,fileName);
        InputStream in=null;
        ByteArrayOutputStream out=null;
        try {
             in = new FileInputStream(file);
             out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length=0;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            return out.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try{
                if(out!=null) {
                    out.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        return null;
    }
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){//如果没有找到'.'则直接在末尾添加.class
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

这段代码有几点需要注意的,注释1处的loadClassData方法会获得class文件的字节码数组,并在注释2处调用defineClass方法将class文件的字节码数组转为Class类的实例。loadClassData方法中需要对流进行操作,关闭流的操作要放在finally语句块中,并且要对in和out分别采用try语句,如果in和out共同在一个try语句中,那么如果in.close()发生异常,则无法执行 out.close()

最后我们来验证DiskClassLoader是否可用,代码如下所示。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {
    public static void main(String[] args) {
        DiskClassLoader diskClassLoader = new DiskClassLoader("D:\\lib");//1
        try {
            Class c = diskClassLoader.loadClass("com.example.Jobs");//2
            if (c != null) {
                try {
                    Object obj = c.newInstance();
                    System.out.println(obj.getClass().getClassLoader());
                    Method method = c.getDeclaredMethod("say", null);
                    method.invoke(obj, null);//3
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

注释1出创建DiskClassLoader并传入要加载类的路径,注释2处加载Class文件,需要注意的是,不要在项目工程中存在名为com.example.Jobs的Java文件,否则就不会使用DiskClassLoader来加载,而是AppClassLoader来负责加载,这样我们定义DiskClassLoader就变得毫无意义。接下来在注释3通过反射来调用Jobs的say方法,打印结果如下:

com.example.DiskClassLoader@4554617c
One more thing

使用了DiskClassLoader来加载Class文件,say方法也正确执行,显然我们的目的达到了。

后记

这一篇文章我们学习了Java中的ClassLoader,包括ClassLoader的类型、双亲委托模式、ClassLoader继承关系以及自定义ClassLoader,为的是就是更好的理解下一篇所要讲解的Android中的ClassLoader。

参考资料
一看你就懂,超详细java中的ClassLoader详解
深入分析Java ClassLoader原理


我的新书《Android进阶之光》已出版,更多成体系的Android相关原创技术干货尽在公众号:刘望舒。

作者:itachi85 发表于2017/9/25 19:42:50 原文链接
阅读:115 评论:0 查看评论

Android进阶---Android Webview重定向问题解决

$
0
0
 项目中需要webview重定向,但是由于一个webveiw里面有许多加载操作,因此在调用webview。goback()方法时,往往达不到我们需要的操作效果。

1.解决方法

WebBackForwardList webBackForwardList=webview.copyBackForwardList()获取webview加载栈,然后更具加载栈做逻辑操作

2.webBackForwardList常用的方法

  • int size = webBackForwardList.getSize()
  • webBackForwardList.getCurrentItem()
  • webBackForwardList.getCurrentIndex()
  • webBackForwardList.getItemAtIndex(index)

getsize()方法获取当前加载栈的长度;
getCurrentItem()获取当前webview所加载的界面,我们可以在这个方法下获得url,title等内容;
getCurrentIndex()获取当前加载在加载栈中的位置;
webBackForwardList.getItemAtIndex(index)获取加载栈中第index页面;

3.合理使用

在2中我们获取到加载页面的一些信息,然后可以根据当前url,标题,位置。来进行相应的处理。

4.Demo使用

现在我们有这样一个场景:打开一个activity,webview加载A界面,然后用户在A里面点击B,然后点击C,点击D。返回操作时我们需要从D跳到B,然后跳A.下面为代码:

  public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (webView.canGoBack()) {
            WebBackForwardList webBackForwardList = webView.copyBackForwardList();
            if (webBackForwardList.getCurrentIndex() == 4) {//当前处于D界面
                webView.loadUrl(webBackForwardList.getItemAtIndex(1).getUrl());//挑转到B界面
                return true;
            }
            webView.goBack();
        }

        return true;
    }
作者:DG_summer 发表于2017/9/26 18:55:55 原文链接
阅读:38 评论:0 查看评论

86. Partition List。

$
0
0

Given a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x.

You should preserve the original relative order of the nodes in each of the two partitions.

For example,
Given 1->4->3->2->5->2 and x = 3,
return 1->2->2->4->3->5.


给定一个数值x,然后将链表中小于x的数字放在前面,大于或者等于x的放在后面,注意不要乱了原来的顺序。也是一道比较简单的题,我的第一个思路就是使用stack的特性,使用两个栈。遍历链表,小于x的放在一个栈中,大于或者等于放在另一个栈中。然后遍历两个栈,如果存放小数值的栈不为空则将该栈中的节点出栈并使用头插法连接起来,然后遍历存放大数值的栈,将其连接起来即可。

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        if(!head) {
            return NULL;
        }

        ListNode* result = new ListNode(0);

        stack<ListNode*> lt;//小于x的
        stack<ListNode*> gt;//大于x的

        while(head) {
            if(head->val < x) {
                lt.push(head);
            } else {
                gt.push(head);
            }
            head = head->next;
        }

        while(!lt.empty() || !gt.empty()) {
            if(!gt.empty()) {//如果大的数没有遍历完则遍历大的
                gt.top()->next = result->next;
                result->next = gt.top();
                gt.pop();
            } else {
                lt.top()->next = result->next;
                result->next = lt.top();
                lt.pop();
            }
        }

        return result->next;
    }
};

第二种方法就是使用两个指针,遍历一遍链表,使用一个指针将小于x的节点连接起来,用另一个节点将大于或者等于x的节点连接起来,最后将两个链表按照小的在前,大的在后的顺序整合起来即可。

#include <iostream>
#include <stack>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {

        ListNode* ltDummy = new ListNode(0);
        ListNode* gtDummy = new ListNode(0);
        ListNode* ltTail = ltDummy;
        ListNode* gtTail = gtDummy;

        while(head) {
            if(head->val < x) {
                ltTail->next = head;
                ltTail = head;
            } else {
                gtTail->next = head;
                gtTail = head;
            }
            head = head->next;
        }

        ltTail->next = gtDummy->next;
        gtTail->next = NULL;//最后一个可能不为空,所以需要赋值为空

        return ltDummy->next;


        /*if(!head) {
            return NULL;
        }

        ListNode* result = new ListNode(0);

        stack<ListNode*> lt;//小于x的
        stack<ListNode*> gt;//大于x的

        while(head) {
            if(head->val < x) {
                lt.push(head);
            } else {
                gt.push(head);
            }
            head = head->next;
        }

        while(!lt.empty() || !gt.empty()) {
            if(!gt.empty()) {//如果大的数没有遍历完则遍历大的
                gt.top()->next = result->next;
                result->next = gt.top();
                gt.pop();
            } else {
                lt.top()->next = result->next;
                result->next = lt.top();
                lt.pop();
            }
        }

        return result->next;*/

    }
};

int main() {
    Solution s;


    ListNode node1(1);
    ListNode node2(4);
    ListNode node3(3);
    ListNode node4(2);
    ListNode node5(5);
    ListNode node6(2);

    node1.next = &node2;
    node2.next = &node3;
    node3.next = &node4;
    node4.next = &node5;
    node5.next = &node6;

    ListNode* p = s.partition(&node1,3);

    while(p) {
        cout << p->val;
        cout << endl;
        p = p->next;
    }

}

作者:Leafage_M 发表于2017/9/26 23:07:12 原文链接
阅读:56 评论:0 查看评论

142. Linked List Cycle II。

$
0
0

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

Note: Do not modify the linked list.

Follow up:
Can you solve it without using extra space?


141. Linked List Cycle。的升级版本。需要找出进入环的那个节点所在。第一种还是使用之前的集合方法,定义一个set集合并遍历链表,如果当前节点不存在set中则将节点存放进去,如果存在了这说明这个节点就是进入环的位置。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_set<ListNode*> nodes;
        while(head) {
            if(nodes.count(head)==1) {
                return head;
            } else {
                nodes.insert(head);
            }
            head = head->next;
        }
        return NULL;
    }
};

第二种使用的是赋值的方法,遍历每个节点,然后将节点的值赋值为INT_MAX,那么遇到下一次的节点值等于INT_MAX时候就是环的入口。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {

        while(head) {
            if(head->val != INT_MAX) {
                head->val = INT_MAX;
            } else {
                return head;
            }
            head = head->next;
        }

        return NULL;
    }
};

第三种是使用快慢指针的方法,先来画一个图。

这里写图片描述

图中分为了三个阶段A为链表的起点,B为环的入口,如果使用快慢指针的话,快的指针先进入环,然后慢的指针进入。这个时候可以看成快的指针在追赶慢的指针,假设快指针在C处追到了慢指针。此时相当于是快指针已经饶了环一圈了,那么快指针走的路就是1+2+3+2,而慢指针走的路是1+2。并且因为快指针走的路是慢指针的两倍,所以能够得出3=1.既然1和3的路是相等的,就可以使用两个指针分别从A点和C点出发,当他们相遇的时候就是走了图中1(或者3)的距离,而从链表的起点A到环的入口点B正好距离就是1,所以此时相遇的节点就是环的入口。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {

        //快慢指针

        if(!head) return nullptr;

        ListNode* slow = head;
        ListNode* fast = head;
        ListNode* entry = head;

        while(fast->next && fast->next->next) {
            fast = fast->next->next;
            slow = slow->next;

            if(fast == slow) {
                while(entry != slow) {
                    entry = entry->next;
                    slow = slow->next;
                }
                return entry;
            }

        }

        return nullptr;
    }
};
作者:Leafage_M 发表于2017/9/26 23:20:10 原文链接
阅读:56 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>