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

iOS开发 非常全面的Swift资料收集

$
0
0

文章转载自我的个人博客原文链接

自言自语: 亿万千百十, 皆起于一

资料1 —教程类

官方文档中文翻译http://wiki.jikexueyuan.com/project/swift/
Github上的地址点我

Using Swift with Cocoa and Objective-C

WWDC 2015

系统化的开发文档iOS Developer Library

Swift 开源及跨平台开发swift.org

斯坦福课程Stanford University: Developing iOS 8 Apps with Swift中文字幕版 By @网易公开课

资料2 —开源库

SwiftyJSON: JSON 解析库
OAuthSwift: 国外主流网站 OAuth 授权类库
Timepiece: 日期加减运算、初始设置、变更以及格式解析
SWXMLHash: XML 解析类库
QueryKit: 功能完善的 CoreData 查询类库
Alamofire: Swift版本AFN
AlamofireImage: 基于 Alamofire 的网络图片组件库
starscream: WebSocket 客户端类库
gifu: 高效显示GIF库
HanekeSwift: 轻量带缓存高性能图片加载组件
Concorde: 一个可用于下载和解码渐进式 JPEG 的库
SwiftOCR: 字母和数字的识别库
AsyncDisplayKit: 提供界面的高流畅性切换及更灵敏的响应
BluetoothKit: 基于 CoreBluetooth API 跨设备间蓝牙通讯封装类库
Chatto: 轻量级聊天应用框架及示例
AudioKit: 音频合成、加工及分析平台框架库
Charts: 图表库
PNChart-Swift: 动画效果的图表控件库
InceptionTouch: 让没有 3D Touch 设备也有类似交互体验

资料3 —应用

edhita
支持 Markdown, HTML 预览的文本编辑器

Markoff
运行于 OS X 基于 cmark 轻量级 Markdown 预览

WWDC
WWDC 2015 应用下载

firefox-ios
来自 Mozilla 开发团队大型纯 Swift 项目

focus
拦截 Safari 的广告内容,分析和社交追踪器等

Carthage
更简单的方式来管理 Cocoa。与 CocoaPods 差别点这里

LeetCode-Solutions
LeetCode 的 Swift 语言版解题方案

CoPilot
协同编程 Xcode 插件

Refactorator
Xcode 代码重构插件,使重命名变量、函数、枚举名等变得方便

iconMaker
Xcode插件自动生成不同尺寸的应用图标

SwiftCov
代码测试覆盖率命令行工具

ViewMonitor
测量视图位置、大小、背景、字体大小等,开发调试神器

Cuckoo
用法更接近于传统单元测试 Mock 框架库

droptogif
视频拖拽到应用窗口后自动转换为 GIF 动画

swiftmi-app
完整的社区应用

Swift-Radio-Pro
集成 LastFM 的专业电台应用

SimpleMemo)
支持 3D Touch 功能易便签

VWInstantRun
即时运行选中的代码片段

BrowserTV
可交互 Apple TV 浏览器

QingDict
轻量级、实用主义的词典程序

shift-js
Swift 程序员在写 iOS 和 Web 应用时无需语言切换

injectionforxcode
修改一个类的代码实现而不用重启整个应用 Xcode 插件

cleartext-mac
提供一千个常用单词的编辑器

Peek
检查界面内组件布局信息

producthunt-osx)
Product Hunt 开源 Mac 客户端

xi-editor
高性能文本编辑器

BuildTimeAnalyzer-for-Xcode
实用的编译时间分析 Xcode 插件

RealmVideo
同步播放 realm.io 网站上的演讲视频和 slides

PodcastMenu
便捷地收听广播 Overcast.fm



除了以上这些我还有一些资料提供, 希望对有需要的同学有帮助.

比较全面的iOS开发资料- 感谢YYKit作者

开源库介绍- 感谢作者

爱开发 - 找到一些你需要的Demo

Facebook Pop进阶 - 感谢作者

还有这个https://www.cocoacontrols.com

感谢提供学习资料的网站, 感谢开源库作者. 如有侵权请联系本人删除.

您可能还对这些感兴趣
Swift版本仿网易云音乐播放音乐动画效果– 简书
三分钟教你把代码托管到Github – 简书
Swift 很强大的图表库-Charts使用 – 简书
Swift版仿简书App淘宝App很友好弹出view效果 – 简书

微博@夏天是个大人了 欢迎你关注我
你还可以加入我创建技术交流群: 498143780 与我交流.

作者:sinat_30162391 发表于2016/11/28 20:51:17 原文链接
阅读:61 评论:0 查看评论

热修复原理(HotFix)初涉

$
0
0

写在最前的话,一直听说热修复,不错,最近修复风靡,不明白原理都不行,明白原理了不会用也不行,故打算拿出一些时间去深入了解一番
翻阅众多资料
在此之前先感谢前人的资料提供,
好了
大家和我一起学习吧;
* 首先明白几个类的加载器:classLoader—->顾名思义,就是用来动态装载class文件的。标准的Java SDK中有个ClassLoader类,借助此类可以装载需要的class文件,前提是ClassLoader类初始化必须制定class文件的路径。
URLClassLoader,
PathClassLoader,
DexLoader
三个加载器使用不同,首先URLClassLoader是不能使用在Android上

PathClassLoader是在APK文件生成之后,及在/data/dalvik/cache文件中存在的,假如文件不存在的话就会爆出ClassNoFindException*()

那最后一个DexClassLoader根据我翻阅资料来看,应该就是基于它了
好接下来我们了解一下名词解析

什么是dex : dex文件是Android平台上可执行文件的类型。这时候我百度的时候出现了一个面试题
这里我简单提及一下,了解的小伙伴可以跳过,不了解的可以点击看一下dex文件和jar文件的区别是什么

区别一:dvm执行的是.dex格式文件  jvm执行的是.class文件   android程序编译完之后生产.class文件,然后,dex工具会把.class文件处理成.dex文件,然后把资源文件和.dex文件等打包成.apk文件。apk就是android package的意思。 jvm执行的是.class文件。
区别二:dvm是基于寄存器的虚拟机  而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。
区别三:.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度

接下来他妈的又有问题了,dvm和jvm的区别
这里写图片描述

嘿,突然发现我居然看懂了,这不就是dex和jar的区别吗,不是同一个名词,但是意思大体一致,为啥,你看呀jar只是将.class文件集成
而我们dex是将.class和资源文件转化成.dex,有木有明白了这几个名词。

好,接下来我们介绍一下DexClassLoder();首先它有一个4个参数的构找方法这里写图片描述
dexPath 是jar,dex文件所在的路径
optimizedDirectory 是得到的dex的文件解压后放置的位置
libraryPath 是文件的library的存放路径,may be null 可是是空啊
parent 父类构造器

讲到这儿其实我们应该有明白,我们其实就是要拿DexClassLoader去进行加载在dexElements遍历之前,去做我们的操作,此时我们还不懂怎么去工作,小伙伴,不要慌,这里我给出一个链接,看完试试能不能明白DexClassLoader的使用原理
dexElements:明白这是啥玩意不,看完再点击上边的链接哈,其中每个dex文件是一个Element,当需要加载类的时候会遍历 dexElements,如果找到类则加载,如果找不到从下一个 dex 文件继续查找。
File.separator 小知识补充(就相当与windows下的“/”Linux下的“\”)

看完回来了,源码真心看不懂,恶心,,我先去吐会,等会回来再说

没看懂,,再去看一会


好了,看得差不多了,其实主要是我们跟踪源码的走向一步一步明白到底去干了什么,不必要知道每一个方法的意义,只要知道他怎么去处理的数据,我在这里就不再赘述了,我们看一下链接中文章的作者对这个DexClassLoader加载的理解,其实静下心来能够看明白的

我们可以简要总结下整个的加载流程,首先是对文件名的修正,后缀名置为”.dex”作为输出文件,然后生个一个DexPathList对象函数直接返回一个DexPathList对象,

在DexPathList的构造函数中调用makeDexElements()函数,在makeDexElement()函数中调用loadDexFile()开始对.dex或者是.jar .zip .apk文件进行处理,

跟入loadDexFile()函数中,会发现里面做的工作很简单,调用optimizedPathFor()函数对optimizedDiretcory路径进行修正。

之后才真正通过DexFile.loadDex()开始加载文件中的数据,其中的加载也只是返回一个DexFile对象。

在DexFile类的构造函数中,重点便放在了其调用的openDexFile()函数,在openDexFile()中调用了openDexFileNative()真正进入native层,

在openDexFileNative()的真正实现中,对于后缀名为.dex的文件或者其他文件(.jar .apk .zip)分开进行处理:

.dex文件调用dvmRawDexFileOpen();其他文件调用dvmJarFileOpen()。

在dvmRawDexFileOpen()函数中,检验dex文件的标志,检验odex文件的缓存名称,之后将dex文件拷贝到odex文件中,并对odex进行优化

调用dvmDexFileOpenFromFd()对优化后的odex文件进行映射,通过mprotect置为"只读"属性并将映射的内存结构保存在DvmDex*结构中。

dvmJarFileOpen()先对文件进行映射,结构保存在ZipArchive中,然后再尝试以文件名作为dex文件名来“打开”文件,

如果失败,则调用dexZipFindEntry在ZipArchive的名称hash表中找名为"class.dex"的文件,然后创建odex文件,下面就和

dvmRawDexFileOpen()一样了,就是对dex文件进行优化和映射。

有没有很开心,最起码懂了一点什么,那我们继续我们的主线

有上边那些我们知道了dex是根据.class文件转化过来的,然后又经dex转化成了odex,而 dex -> odex 则是针对不同平台,不同手机的硬件配置做针对性的优化。就是在这一过程中,虚拟机在启动优化的时候,会有一个选项就是 verify 选项,当 verify 选项被打开的时候,就会执行一次校验,校验的目的是为了判断,这个类是否有引用其他 dex 中的类,如果没有,那么这个类会被打上一个 CLASS_ISPREVERIFIED 的标志。一旦被打上这个标志,就无法再从其他 dex 中替换这个类了。而这个选项开启,则是由虚拟机控制的。


下一步
知道大体意思了,但是不明白怎么去操作这东西,有没有力不从心的感觉,马丹,意味着我还要继续看下去
可是,我发现我看跑题了,看到了一个插件制作的小Demo,卧槽,我还是决定提出来,因为这个看完你能更明白DexClassLoader的工作原理
插件架构Demo
代码简单说明
热修复的详解

来个结尾:你可以告诉别人你懂原理了,牛逼否

看了这么一圈终于知道原理了,当别人问起来你对HotFix的理解的时候,你可以潇洒的说
1,三个ClassLoader的区别,我们采用那个DexClassLoader加载器进行加载
2,这个DexClassLoader有一个四个参数的构造函数,
- 对应着jar,class文件所在的文件位置,
- 对应着要指定获取到的dex文件放置的位置,
- 对应本地文件C,C++ 等本地文件的放置位置,一般设定为null
- 获取到父类加载器,等等我们是看过源码的淫,这里可以来点文章
3,对dex和jar的区别来点事,dex是将dvm将.class文件和资源文件加载成.dex,而那jar是jvm将.class 文件转为.jar.
4,DexClassLoader的工作原理是根据传入的路径获取到dex,如果不是dex的话源码中处理方式,比如讲不是.dex的结尾文件装华为.dex,是的就直接返回进行loadDex();
5,上边扯了这么大牛逼,我们回归正题,如何进行热修复,DexClassLoader是将获取到一个DexEleMents[]的数组,进行遍历,这样就好办了不是,数组是可以添加删除的,嘿嘿,在他遍历之前我们对数组添加,这不就解决了吗,至于真么添加,根据宿主App,拿到App中的所有类,这里我们对类进行过滤,拿到需要实现的类,通过ClassReader,ClassWriter,ClassVIstor,去操作这个字节码文件
6,这里会引出一个问题,就是我们的App都是经过混淆之后发布的,所以这里我们呢,就可以在发布BUG版本保存的,在 Gradle 插件编译过程中,有一个proguardTask,看名字应该就知道他是负责 proguard 任务的,我们可以保存首次执行时的混淆规则(也就是线上出BUG的包),这个混淆规则保存在工程目录中的一个mapping文件,当我们需要执行热修复补丁生成的时候,将线上包的mapping规则拿出来应用到本次编译中,就可以生成混淆后的类跟线上混淆后的类相同的类名的补丁了。


说在最后的话,大哥大姐过年好啊,转载之前给个赞啊

作者:qq_16666847 发表于2016/11/28 21:06:28 原文链接
阅读:22 评论:0 查看评论

分页, 上拉刷新、下拉加载。

$
0
0

一、自定义listview

package com.fragment.home;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.example.virtualaccount.R;
public class CustomRefreshListView extends ListView implements OnScrollListener{
	/**
	 * 头布局
	 */
	private View headerView;
	/**
	 * 头部布局的高度
	 */
	private int headerViewHeight;
	/**
	 * 头部旋转的图片
	 */
	private ImageView iv_arrow;
	/**
	 * 头部下拉刷新时状态的描述
	 */
	private TextView tv_state;
	/**
	 * 下拉刷新时间的显示控件
	 */
	private TextView tv_time;
	/**
	 * 底部布局
	 */
	private View footerView;
	/**
	 * 底部旋转progressbar
	 */
	private ProgressBar pb_rotate;

	/**
	 * 底部布局的高度
	 */
	private int footerViewHeight;


	/**
	 * 按下时的Y坐标
	 */
	private int downY;
	
	private final int PULL_REFRESH = 0;//下拉刷新的状态
	private final int RELEASE_REFRESH = 1;//松开刷新的状态
	private final int REFRESHING = 2;//正在刷新的状态

	/**
	 * 当前下拉刷新处于的状态
	 */
	private int currentState = PULL_REFRESH;

	/**
	 * 头部布局在下拉刷新改变时,图标的动画
	 */
	private RotateAnimation upAnimation,downAnimation;

	/**
	 * 当前是否在加载数据
	 */
	private boolean isLoadingMore = false;

	public CustomRefreshListView(Context context) {
		this(context,null);
	}

	public CustomRefreshListView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}
	
	private void init(){
		//设置滑动监听
		setOnScrollListener(this);
		//初始化头布局
		initHeaderView();
		//初始化头布局中图标的旋转动画
		initRotateAnimation();
		//初始化为尾布局
		initFooterView();
	}
	
	/**
	 * 初始化headerView
	 */
	private void initHeaderView() {
		headerView = View.inflate(getContext(), R.layout.head_custom_listview, null);
		iv_arrow = (ImageView) headerView.findViewById(R.id.iv_arrow);
		pb_rotate = (ProgressBar) headerView.findViewById(R.id.pb_rotate);
		tv_state = (TextView) headerView.findViewById(R.id.tv_state);
		tv_time = (TextView) headerView.findViewById(R.id.tv_time);

		//测量headView的高度
		headerView.measure(0, 0);
		//获取高度,并保存
		headerViewHeight = headerView.getMeasuredHeight();
		//设置paddingTop = -headerViewHeight;这样,该控件被隐藏
		headerView.setPadding(0, -headerViewHeight, 0, 0);
		//添加头布局
		addHeaderView(headerView);
	}
	
	/**
	 * 初始化旋转动画
	 */
	private void initRotateAnimation() {

		upAnimation = new RotateAnimation(0, -180, 
				RotateAnimation.RELATIVE_TO_SELF, 0.5f,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f);
		upAnimation.setDuration(300);
		upAnimation.setFillAfter(true);

		downAnimation = new RotateAnimation(-180, -360, 
				RotateAnimation.RELATIVE_TO_SELF, 0.5f,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f);
		downAnimation.setDuration(300);
		downAnimation.setFillAfter(true);
	}

	//初始化底布局,与头布局同理
	private void initFooterView() {
		footerView = View.inflate(getContext(), R.layout.foot_custom_listview, null);
		footerView.measure(0, 0);
		footerViewHeight = footerView.getMeasuredHeight();
		footerView.setPadding(0, -footerViewHeight, 0, 0);
		addFooterView(footerView);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			//获取按下时y坐标
			downY = (int) ev.getY();
			break;
		case MotionEvent.ACTION_MOVE:
			
			if(currentState==REFRESHING){
				//如果当前处在滑动状态,则不做处理
				break;
			}
			//手指滑动偏移量
			int deltaY = (int) (ev.getY() - downY);

			//获取新的padding值
			int paddingTop = -headerViewHeight + deltaY;
			if(paddingTop>-headerViewHeight && getFirstVisiblePosition()==0){
				//向下滑,且处于顶部,设置padding值,该方法实现了顶布局慢慢滑动显现
				headerView.setPadding(0, paddingTop, 0, 0);

				if(paddingTop>=0 && currentState==PULL_REFRESH){
					//从下拉刷新进入松开刷新状态
					currentState = RELEASE_REFRESH;
					//刷新头布局
					refreshHeaderView();
				}else if (paddingTop<0 && currentState==RELEASE_REFRESH) {
					//进入下拉刷新状态
					currentState = PULL_REFRESH;
					refreshHeaderView();
				}
				super.onTouchEvent(ev);	
				return true;//拦截TouchMove,不让listview处理该次move事件,会造成listview无法滑动
			}
			
			
			break;
		case MotionEvent.ACTION_UP:
			if(currentState==PULL_REFRESH){
				//仍处于下拉刷新状态,未滑动一定距离,不加载数据,隐藏headView
				headerView.setPadding(0, -headerViewHeight, 0, 0);
			}else if (currentState==RELEASE_REFRESH) {
				//滑倒一定距离,显示无padding值的headcView
				headerView.setPadding(0, 0, 0, 0);
				//设置状态为刷新
				currentState = REFRESHING;

				//刷新头部布局
				refreshHeaderView();
				
				if(listener!=null){
					//接口回调加载数据
					listener.onPullRefresh();
				}
			}
			break;
		}
		return super.onTouchEvent(ev);
	}
	
	/**
	 * 根据currentState来更新headerView
	 */
	private void refreshHeaderView(){
		switch (currentState) {
		case PULL_REFRESH:
			tv_state.setText("下拉刷新");
			iv_arrow.startAnimation(downAnimation);
			break;
		case RELEASE_REFRESH:
			tv_state.setText("松开刷新");
			iv_arrow.startAnimation(upAnimation);
			break;
		case REFRESHING:
			iv_arrow.clearAnimation();//因为向上的旋转动画有可能没有执行完
			iv_arrow.setVisibility(View.INVISIBLE);
			pb_rotate.setVisibility(View.VISIBLE);
			tv_state.setText("正在刷新...");
			break;
		}
	}
	
	/**
	 * 完成刷新操作,重置状态,在你获取完数据并更新完adater之后,去在UI线程中调用该方法
	 */
	public void completeRefresh(){
		if(isLoadingMore){
			//重置footerView状态
			footerView.setPadding(0, -footerViewHeight, 0, 0);
			isLoadingMore = false;
		}else {
			//重置headerView状态
			headerView.setPadding(0, -headerViewHeight, 0, 0);
			currentState = PULL_REFRESH;
			pb_rotate.setVisibility(View.INVISIBLE);
			iv_arrow.setVisibility(View.VISIBLE);
			tv_state.setText("下拉刷新");
			tv_time.setText("最后刷新:"+getCurrentTime());
		}
	}
	
	/**
	 * 获取当前系统时间,并格式化
	 * @return
	 */
	private String getCurrentTime(){
		SimpleDateFormat format = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
		return format.format(new Date());
	}
	
	private OnRefreshListener listener;
	public void setOnRefreshListener(OnRefreshListener listener){
		this.listener = listener;
	}
	public interface OnRefreshListener{
		void onPullRefresh();
		void onLoadingMore();
	}
	
	/**
	 * SCROLL_STATE_IDLE:闲置状态,就是静止状态
	 * SCROLL_STATE_TOUCH_SCROLL:手指触摸滑动,就是按着来滑动
	 * SCROLL_STATE_FLING:快速滑动后松开
	 */
	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		if(scrollState==OnScrollListener.SCROLL_STATE_IDLE 
				&& getLastVisiblePosition()==(getCount()-1) &&!isLoadingMore){
			isLoadingMore = true;			
			footerView.setPadding(0, 0, 0, 0);//显示出footerView
			setSelection(getCount());//让listview最后一条显示出来
			
			if(listener!=null){
				listener.onLoadingMore();
			}
		}
	}
	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		// TODO Auto-generated method stub
		
	}
}

二、监听listview

listview.setOnRefreshListener(new OnRefreshListener() {
                      //上啦 刷新,刷新是把集合倒下序,最新的就显示在最上面
			@Override
			public void onPullRefresh() {
				// TODO Auto-generated method stub
				page+=1;
				FilterSelect("",page);//加载数据方法   用参数page来进行分页和后台协商好一页传几条数据
				listview.completeRefresh();
				flag=true;			
			}
  //下拉加载 加载延迟设置为1秒
			@Override
			public void onLoadingMore() {
				listview.postDelayed(new Runnable() {

					@Override
					public void run() {
						page+=1;
						FilterSelect("",page);
						listview.completeRefresh();
					}
				}, 1000L);

			}
		});
//发送请求 填充数据
private void FilterSelect(String key,int page){
		url=url+"&type="+key+"&page="+page;
		System.out.println(url);
		StringRequest param=new StringRequest(Method.GET,url,new Response.Listener<String>() {
			@Override
			public void onResponse(String response) {
				// TODO Auto-generated method stub	
				Gson gson=new Gson();				 	
				root=gson.fromJson(response.toString(), GoodsRoot.class);
				if(root.status==1)
				{
					//     	  data=root.data;
					//     	  addData=data;
					if(flag){
						List<GoodsDataBean> data=root.data;
						Collections.reverse(data);
						addData.addAll(data);
					}
					else addData.addAll(root.data);


					adapter=new GameDetailsAdapter(ProductDetails.this,addData);
					listview.setAdapter(adapter);


				}
				else 
					Toast.makeText(ProductDetails.this, "暂无最新商品", Toast.LENGTH_SHORT).show();
			}
		},null);
		requestdata.add(param);
		url=upurl;
	}




作者:en_wei 发表于2016/11/28 21:21:59 原文链接
阅读:29 评论:0 查看评论

Android基础知识点-Manifest清单文件

$
0
0

每个应用的根目录中都必须包含一个 AndroidManifest.xml 文件(且文件名精确无误)。 清单文件向 Android 系统提供应用的必要信息,系统必须具有这些信息方可运行应用的任何代码。

清单文件还可执行以下操作:

为应用的 Java 软件包命名。软件包名称充当应用的唯一标识符。
描述应用的各个组件,包括构成应用的 Activity、服务、广播接收器和内容提供程序。它还为实现每个组件的类命名并发布其功能,例如它们可以处理的 Intent 消息。这些声明向 Android 系统告知有关组件以及可以启动这些组件的条件的信息。
确定托管应用组件的进程。
声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互。还声明其他应用与该应用组件交互所需具备的权限
列出 Instrumentation 类,这些类可在应用运行时提供分析和其他信息。这些声明只会在应用处于开发阶段时出现在清单文件中,在应用发布之前将移除。
声明应用所需的最低 Android API 级别
列出应用必须链接到的库

注:

准备要在 Chromebook 上运行的 Android 应用时,要考虑一些重要的硬件和软件功能限制。

清单文件结构

下面的代码段显示了清单文件的通用结构及其可包含的每个元素。每个元素及其所有属性全部记录在一个单独的文件中。

提示:要查看本文档提及的任何元素的详细信息,只需点按元素名称。

下面是清单文件的示例:

<?xml version="1.0" encoding="utf-8"?>

<manifest>

    <uses-permission />
    <permission />
    <permission-tree />
    <permission-group />
    <instrumentation />
    <uses-sdk />
    <uses-configuration />  
    <uses-feature />  
    <supports-screens />  
    <compatible-screens />  
    <supports-gl-texture />  

    <application>

        <activity>
            <intent-filter>
                <action />
                <category />
                <data />
            </intent-filter>
            <meta-data />
        </activity>

        <activity-alias>
            <intent-filter> . . . </intent-filter>
            <meta-data />
        </activity-alias>

        <service>
            <intent-filter> . . . </intent-filter>
            <meta-data/>
        </service>

        <receiver>
            <intent-filter> . . . </intent-filter>
            <meta-data />
        </receiver>

        <provider>
            <grant-uri-permission />
            <meta-data />
            <path-permission />
        </provider>

        <uses-library />

    </application>

</manifest>

以下列表包含可出现在清单文件中的所有元素,按字母顺序列出:

action
activity
activity-alias
application
category
data
grant-uri-permission
instrumentation
intent-filter
manifest
meta-data
permission
permission-group
permission-tree
provider
receiver
service
supports-screens
uses-configuration
uses-feature
uses-library
uses-permission
uses-sdk
注:这些是仅有的合法元素 – 您无法添加自己的元素或属性。

文件约定
本节描述普遍适用于清单文件中所有元素和属性的约定和规则。

元素

只有 manifest 和 application>元素是必需的,它们都必须存在并且只能出现一次。其他大部分元素可以出现多次或者根本不出现。但清单文件中必须至少存在其中某些元素才有用。
如果一个元素包含某些内容,也就包含其他元素。所有值均通过属性进行设置,而不是通过元素内的字符数据设置。

同一级别的元素通常不分先后顺序。例如,activity、provider 和 service 元素可以按任何顺序混合在一起。这条规则有两个主要例外:

activity-alias元素必须跟在别名所指activity 之后。
application 元素必须是 manifest 元素内最后一个元素。换言之,/manifest 结束标记必须紧接在 /application结束标记后。

属性

从某种意义上说,所有属性都是可选的。但是,必须指定某些属性,元素才可实现其目的。请使用本文档作为参考。对于真正可选的属性,它将指定默认值或声明缺乏规范时将执行何种操作。
除了根 manifest 元素的一些属性外,所有属性名称均以 android: 前缀开头。例如,android:alwaysRetainTaskState。由于该前缀是通用的,因此在按名称引用属性时,本文档通常会将其忽略。

声明类名

许多元素对应于 Java 对象,包括应用本身的元素(application 元素)及其主要组件:Activity (activity)、服务 (service)、广播接收器 (receiver) 以及内容提供程序 (provider)。
如果按照您针对组件类(Activity、Service 和BroadcastReceiverContentProvider)几乎一直采用的方式来定义子类,则该子类需通过 name 属性来声明。该名称必须包含完整的软件包名称。例如,Service 子类可能会声明如下:

<manifest . . . >
    <application . . . >
        <service android:name="com.example.project.SecretService" . . . >
            . . .
        </service>
        . . .
    </application>
</manifest>

但是,如果字符串的第一个字符是句点,则应用的软件包名称(如 元素的 package 属性所指定)将附加到该字符串。以下赋值与上述方法相同:

<manifest package="com.example.project" . . . >
    <application . . . >
        <service android:name=".SecretService" . . . >
            . . .
        </service>
        . . .
    </application>
</manifest>

当启动组件时,Android 系统会创建已命名子类的实例。如果未指定子类,则会创建基类的实例。

多个值

如果可以指定多个值,则几乎总是在重复此元素,而不是列出单个元素内的多个值。例如,intent 过滤器可以列出多个操作:

<intent-filter . . . >
    <action android:name="android.intent.action.EDIT" />
    <action android:name="android.intent.action.INSERT" />
    <action android:name="android.intent.action.DELETE" />
    . . .
</intent-filter>

资源值

某些属性的值可以显示给用户,例如,Activity 的标签和图标。这些属性的值应该本地化,并通过资源或主题进行设置。资源值用以下格式表示:

@[<i>package</i>:]<i>type</i>/<i>name</i>

如果资源与应用在同一个软件包中,可以省略软件包名称。类型是资源类型,例如字符串或可绘制对象,名称是标识特定资源的名称。下面是示例:

activity android:icon=”@drawable/smallPic” …
主题中的值用类似的方法表示,但是以 ? 开头,而不是以 @ 开头:

?[<i>package</i>:]<i>type</i>/<i>name</i>

字符串值

如果属性值为字符串,则必须使用双反斜杠 (\) 转义字符,例如,使用 \n 表示换行符或使用 \uxxxx 表示 Unicode 字符。
文件功能
下文介绍在清单文件中体现某些 Android 特性的方式。

Intent 过滤器

应用的核心组件(例如其 Activity、服务和广播接收器)由 intent 激活。Intent 是一系列用于描述所需操作的信息(Intent 对象),其中包括要执行操作的数据、应执行操作的组件类别以及其他相关说明。Android 系统会查找合适的组件来响应 intent,根据需要启动组件的新实例,并将其传递到 Intent 对象。

组件将通过 intent 过滤器公布它们可响应的 intent 类型。由于Android 系统在启动某组件之前必须了解该组件可以处理的 intent,因此 intent 过滤器在清单中被指定为 元素。一个组件可有任意数量的过滤器,其中每个过滤器描述一种不同的功能。

显式命名目标组件的 intent 将激活该组件,因此过滤器不起作用。不按名称指定目标的 intent 只有在能够通过组件的一个过滤器时才可激活该组件。

如需了解有关如何根据 intent 过滤器测试 Intent 对象的信息,请参阅 Intent 和 Intent 过滤器文档。

图标和标签

对于可以显示给用户的小图标和文本标签,大量元素具有 icon 和 label 属性。此外,对于同样可以显示在屏幕上的较长说明文本,某些元素还具有 description 属性。例如, 元素具有所有这三个属性。因此,当系统询问用户是否授权给请求获得权限的应用时,权限图标、权限名称以及所需信息的说明均会呈现给用户。

无论何种情况下,在包含元素中设置的图标和标签都将成为所有容器子元素的默认 icon 和 label 设置。因此,在 元素中设置的图标和标签是每个应用组件的默认图标和标签。同样,为组件(例如 元素)设置的图标和标签是组件每个 元素的默认设置。如果 元素设置标签,但是 Activity 及其 intent 过滤器不执行此操作,则应用标签将被视为 Activity 和 intent 过滤器的标签。

在实现过滤器公布的功能时,只要向用户呈现组件,系统便会使用为 intent 过滤器设置的图标和标签表示该组件。例如,具有 android.intent.action.MAIN 和 android.intent.category.LAUNCHER 设置的过滤器将 Activity 公布为可启动应用的功能,即,公布为应显示在应用启动器中的功能。在过滤器中设置的图标和标签显示在启动器中。

权限

权限是一种限制,用于限制对部分代码或设备上数据的访问。施加限制是为了保护可能被误用以致破坏或损害用户体验的关键数据和代码。

每种权限均由一个唯一的标签标识。标签通常指示受限制的操作。以下是 Android 定义的一些权限:

android.permission.CALL_EMERGENCY_NUMBERS
android.permission.READ_OWNER_DATA
android.permission.SET_WALLPAPER
android.permission.DEVICE_POWER
一个功能只能由一种权限保护。

如果应用需要访问受权限保护的功能,则必须在清单文件中使用 元素声明应用需要该权限。将应用安装到设备上之后,安装程序会通过检查签署应用证书的颁发机构并(在某些情况下)询问用户,确定是否授予请求的权限。如果授予权限,则应用能够使用受保护的功能。否则,其访问这些功能的尝试将会失败,并且不会向用户发送任何通知。

应用也可以使用权限保护自己的组件。它可以采用由 Android 定义(如 android.Manifest.permission 中所列)或由其他应用声明的任何权限。它也可以定义自己的权限。新权限用 元素来声明。例如,Activity 可受到如下保护:

<manifest . . . >
    <permission android:name="com.example.project.DEBIT_ACCT" . . . />
    <uses-permission android:name="com.example.project.DEBIT_ACCT" />
    . . .
    <application . . .>
        <activity android:name="com.example.project.FreneticActivity"
                  android:permission="com.example.project.DEBIT_ACCT"
                  . . . >
            . . .
        </activity>
    </application>
</manifest>

请注意,在此示例中,DEBIT_ACCT 权限不仅是通过 permission元素来声明,而且其使用也是通过 uses-permission 元素来请求。要让应用的其他组件也能够启动受保护的 Activity,您必须请求其使用权限,即便保护是由应用本身施加的亦如此。

同样还是在此示例中,如果将 permission 属性设置为在其他位置(例如,android.permission.CALL_EMERGENCY_NUMBERS)声明的权限,则无需使用 permission 元素再次声明。但是,仍有必要通过 uses-permission 请求其使用权限。

permission-tree元素声明为代码中定义的一组权限声明命名空间,permission-group为一组权限定义标签,包括在清单文件中使用 permission元素声明的权限以及在其他位置声明的权限。这只影响如何对提供给用户的权限进行分组。permission-group元素并不指定属于该组的权限,而只是为组提供名称。可通过向 permission元素的 permissionGroup 属性分配组名,将权限放入组中。

每个应用均链接到默认的 Android 库,该库中包括用于开发应用(以及通用类,如 Activity、服务、intent、视图、按钮、应用、ContentProvider)的基本软件包。

但是,某些软件包驻留在自己的库中。如果应用使用来自其中任一软件包的代码,则必须明确要求其链接到这些软件包。清单文件必须包含单独的 uses-library 元素来命名其中每个库。库名称可在软件包的文档中找到。

我的微信二维码如下,欢迎交流讨论

这里写图片描述

欢迎关注《IT面试题汇总》微信订阅号。每天推送经典面试题和面试心得技巧

微信订阅号二维码如下:

这里写图片描述

作者:u010321471 发表于2016/11/28 21:49:08 原文链接
阅读:107 评论:0 查看评论

Android开发——布局性能优化的一些技巧(一)

$
0
0

0. 前言

上一篇我们分析了为什么LinearLayout会比RelativeLayout性能更高,意义在于分析了这两种布局的实现源码,算是对一个小结论的证明过程,但是对布局性能的优化效果,对这两种布局的选择远不如减少布局层级、避免过分绘制、按需加载等效果明显。所以本篇将着重总结Android布局性能优化的各种技巧。本文原创,转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52923827

 

1.   <include/>

<include>标签可以在一个布局中引入另外一个布局,通常适合于界面布局复杂、不同界面有共用布局的APP中,比如顶部布局、侧边栏布局、底部Tab栏布局、ListViewitem布局等,将这些公共布局抽取出来再通过<include>标签引用,既可以使代码结构清晰,又可统一修改使用

比如先写一个公共的标题栏标题栏title_bar.xml(这里就不具体实现了),并在我们的主xml文件里调用<include>来使用这个公共布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/title_bar"/>
</RelativeLayout>

当然include也可以使用layout属性来设置布局文件的宽高和位置,但需要注意的是,必须要复写android:layout_widthandroid:layout_height属性才能使用其它属性(如:android:layout_grivityandroid:layout_align...android:id等),这样可以避免include引用中的子组件属性影响到include的布局效果。

比如下面这个例子给我们include进的组件设置高度和位置:

<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 <RelativeLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_margin="10dp">
     <include layout="@layout/title_bar" />
     <include
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_alignParentBottom="true"
        layout="@layout/ title_bar"/>
 </RelativeLayout>
</RelativeLayout>


2.   减少嵌套

这个问题我们在LinearLayout和RelativeLayout的性能对对比中已经解释过了,在不响应层级深度的情况下,使用Linearlayout而不是RelativeLayout。为开发者默认新建RelativeLayout是希望开发者能采用尽量少的View层级,因为很多效果是需要多层LinearLayout的嵌套,这必然不如一层的RelativeLayout性能更好。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客


3.  <merge/>

<merge/>标签通过减少View树的层级来优化Android的布局。先来用个例子演示一下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent" >
	<TextView
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="merge标签使用" />
</RelativeLayout>

运行后使用“DDMS -> DumpView Hierarchy for UI Automator”工具,截图如下:

 

最下面两层RelativeLayoutTextView就是布局中的内容,上面的FrameLayoutActivitysetContentView添加的顶层视图。下面我们将上述布局代码中的RelativeLayout修改为merge标签再查看层级结构如下:

 

1)从结果来看,FrameLayout下面直接就是TextView与之前的相比少了一层RelativeLayout但效果相同。这个例子中TextView不需要指定任何针对父视图的布局属性,只用于添加到父视图上并显示,这种情况就可以使用<merge/>标签优化。但是我们一般很少遇到这种情况。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客

2)更多的<merge/>标签使用情景是在LinearLayout里面嵌入一个布局(比如使用了include),而恰恰这个布局的根节点也是LinearLayout,这样就多了一层没有用的嵌套,增加了View深度,这个时候如果我们使用merge根标签就修饰被嵌入的布局的根标签就可以避免此问题。


4.   ViewStub

一个最最最可能使用到的场景就是请求网络加载列表,如果网络异常或者加载失败,我们可以显示一个用于提示用户的View,上面可以点击重新加载。当网络正常时,我们就没有理由显示这个提示View。但是如果我们通过代码逻辑这种方式实现动态更改这个View的可见性(GONE或者VISIBLE),在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化且耗费内存资源

为了解决这个性能问题,ViewStub应运而生,ViewStub是一个轻量级的View,看不见、不占布局位置、占用资源非常小。ViewStub被设置为可见或调用了ViewStub.inflate()的时候,ViewStub所指向的布局才会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局,ViewStub控件本身就不存在了(ViewStub对象会被置空),取而代之的是被inflateLayout,因此它也被称做惰性控件。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客

综上ViewStub的原理,就可以使用它来方便的在运行时,决定要不要显示某个布局。使用实例如下:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    ……
    <ViewStub
        android:layout_gravity="center"
        android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/hint_fail_view"
        android:inflatedId="@+id/hint_fail_view"
        android:layout="@layout/fail_view"/>
</merge>

android:layout="@layout/fail_view"指向页面加载失败的布局文件,里面包含一个idtvTextView

当出现网络异常时,我们在代码里这样使用ViewStub

private View hintFailView;
if (网络异常) {
    if (hintFailView == null) {
        ViewStub viewStub = (ViewStub)this.findViewById(R.id.hint_fail_view);
        hintFailView = viewStub.inflate(); //注意这里
        TextView textView = (TextView) hintFailView.findViewById(R.id.tv);
        textView.setText("网络异常");
    }
    hintFailView.setVisibility(View.VISIBLE);
}else{
    //网络正常
    if (hintFailView!= null) {
        hintFailView.setVisibility(View.GONE);
    }
    //业务逻辑
}


5.  避免OverDraw

一个简单的过度绘制例子是父控件和其上的子控件都设置了Background,那么人们是看不到被子控件所覆盖的那部分父控件背景的,这就造成了OverDraw,我们可以通过设置-开发者选项-显示GPU过度绘制来查看应用是否存在严重的OverDraw问题。



如果你发现应用中有些色块为红色,那么你可要去优化它了,你需要去根据颜色提示去找到你过度绘制的地方,需要注意的是在我们有自己的背景色的情况下,顶层View的背景色我们可以置空来优化。

setContentView(R.layout.activity_overdraw_01);
getWindow().setBackgroundDrawable(null);

除了去除不必要的背景色,还有一个防止OverDraw的方法,那就是使用画布的clipRect()方法来去除自定义View中不必要的重叠绘制
在看clipRect方法之前先看看如果将res目录下的图片文件转换为bitmap对象,这里总结了两种方法,大家可以参考使用:

InputStream is = this.getContext().getResources().openRawResource(R.drawable.icon);  
Bitmap mBitmap = BitmapFactory.decodeStream(is);  
//或者使用BitmapDrawable
Bitmap mBitmap = new BitmapDrawable(is).getBitmap();

clipRect方法可以截取画布中的一个矩形区域,在此区域外的将不再绘制显示。实例如下:

/*
*author SEU_Calvin in 2016/10
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = mBmp.getWidth();
int height = mBmp.getHeight();

canvas.save();
mPaint.setColor(Color.CYAN);
//先在屏幕的0,0处绘制一个与我们Bitmap宽高相等的蓝色矩形
canvas.drawRect(0, 0, width, height, mPaint);
canvas.restore();
 
canvas.save();
//裁剪画布,左上角为0,0  右下角为指定宽高的2倍和1.5倍
canvas.clipRect(0, 0, width*2, height*3/2);
//以width,height为左上角绘制我们的Bitmap,由于图片的下半部分在裁剪画布之外所以不显示
canvas.drawBitmap(mBmp, width, height, mPaint);
canvas.restore();
}

结果如下所示,工作过程已经在代码的注释里写的很清楚了。



6.  其他小技巧

为了控制篇幅,将一些看了让人感到惊艳的布局优化小技巧总结分享到了布局性能优化的一些技巧(二),希望可以帮助到你~


最后希望各位看官老爷们多点赞支持~


作者:SEU_Calvin 发表于2016/11/28 21:55:18 原文链接
阅读:26 评论:0 查看评论

Android开发——布局性能优化的一些技巧(二)

$
0
0

0. 前言

上一篇我们介绍了布局性能优化里常用的技巧,比如减少布局层级、按需加载、合并等技巧。这篇是受唯鹿的博客的启发,总结的一些Android布局优化的小技巧,虽然不能达到立竿见影的布局优化效果,毕竟优化是一个持之以恒的过程,但是看完一定会带给你耳目一新的感觉,定会有所收获。本文原创,转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52924008


1.       TextView同时显示图片和文字

首先来看一下下面这张效果图:


要实现这个效果,先做其中的一个条目。比如上图中“我的卡劵”部分。很多人是不是想到写一个水平的LinearLayout,并分别填入ImageViewTextView以及最后面的ImageView呢。其实这样写效率是很低的。我们其实可以用TextView同时显示图片和文字。实现如下:

<?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="match_parent">

    <TextView
        android:drawableLeft="@drawable/icon_1"
        android:drawableRight="@drawable/icon_4"
        android:drawablePadding="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textSize="16sp"
        android:text="我的卡券"
        android:background="@color/white"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="50dp" />
</LinearLayout>

效果图:


结果肯定是和一开始的想法所展现的结果是一样的,但是显然少了两个ImageView和去除嵌套LinearLayout。达到了布局优化的目的。当然drawableTopdrawableBottom这两个属性也可以根据需求使用。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客


2.    使用CompoundDrables

上面的效果我们在代码里也可以通过CompoundDrawables实现,利用代码setCompoundDrawables(Drawableleft, Drawable top, Drawable right, Drawable bottom)可以让我们动态去设置图片。下面看一下官方API说明和使用示例:

//Sets the Drawables (if any) to appear to the left of, above, to the right of, and below the text. 
//Use null if you do not want a Drawable there. The Drawables must already have had setBounds(Rect) called.
//可以在上、下、左、右设置图标,如果不想在某个地方显示,则设置为null
//但是Drawable必须已经setBounds(Rect)
//意思是你要添加的资源必须已经设置过初始位置、宽和高等信息
Drawable drawable= getResources().getDrawable(R.drawable.res);
drawable.setBounds( 0, 0, drawable.getMinimumWidth(),dra.getMinimumHeight());
tv.setCompoundDrawables(null, null, drawable, null);


3.   使用LinearLayout自带的分割线

继续实现一开始的效果图,如下图中两个条目之间是有一个灰色的间隔的:



我以前是用一个带颜色和高度的View来实现的,相信一定有人和我一样。但是相信使用LinearLayout自带的分割线一定会效率更高一些。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:divider="@drawable/divider"
    android:showDividers="middle">
</LinearLayout>

核心部分就是android:divider="@drawable/divider",我们在divider.xml中自己画一个带颜色的矩形即可。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <size android:width="1dp"
          android:height="1dp"/>
    <solid android:color="#e1e1e1"/>
</shape>

showDividers 是分隔线的显示位置,beginningmiddleend分别代表显示在开始、中间、末尾。

还有dividerPadding属性这里没有用到,意思很明确给divider添加padding,感兴趣的同学可以试试。


4.       使用Space控件

最后在上面效果图中的“地址管理”和“检查更新”之间,我们需要留一部分空白,效果图如下:



传统的做法有两种:

1)添加一个高10dp的透明View

2)使用android:layout_marginTop="10dp"

增加View违背了我们优化布局性能的初衷,使用过多的margin会影响代码的可读性,这时你就可以使用Space,他是一个轻量级的View。它的源码的draw方法中没有绘制任何东西,那么性能肯定是比传统的做法要好啦。使用起来也很简单,在两个TextView之间添加如下代码即可。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客

<Space
    android:layout_width="match_parent"
    android:layout_height="15dp"/>


5.    使用TextView的行间距

先看一下要实现的效果图:


4TextView,并为它们设置一个垂直的子LinearLayout当然可以实现,但是我们可以通过设置TextView的行间距来进行优化:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="100dp"
    android:background="@color/white"
    android:layout_width="match_parent">
    <ImageView
        android:padding="25dp"
        android:src="@drawable/kd_1"
        android:layout_width="100dp"
        android:layout_height="match_parent"/>
    <TextView
        android:textSize="14dp"
        android:lineSpacingExtra="8dp"
        android:gravity="center_vertical"
        android:text="揽件方式:上门取件\n快递公司:顺丰快递\n预约时间:9月6日 立即取件\n快递费用:等待称重确定价格"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

可以看到我们仅仅利用android:lineSpacingExtra="8dp"这一行代码就省去了3TextView

lineSpacingExtra属性代表的是行间距,默认是0,是一个绝对高度值,同时还有lineSpacingMultiplier属性,它代表行间距倍数,默认为1.0f,是一个相对高度值。如果两者同时设置高度计算规则为mTextPaint.getFontMetricsInt(null) * 行间距倍数 + 行间距。


6.    使用Spannable

先看一下要实现的效果图:



如果实现上图红框中的效果,笨办法就是写三个TextView价格门市价分别实现,其实用一个TextView就可以实现:

String text = String.format("¥%1$s  门市价:¥%2$s", 18.6, 22);
int z = text.lastIndexOf("门");
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
ssb.setSpan(new AbsoluteSizeSpan(DisplayUtil.dip2px(mContext,14)), 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); //字号
ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#afafaf")), z, text.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 
ssb.setSpan(new AbsoluteSizeSpan(DisplayUtil.dip2px(mContext,14)), z, text.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); //字号
tv.setText(ssb);

如果不熟悉String.format()的使用,可以参考这篇博客

下面简单介绍一下第5行代码的参数含义:参数1表示颜色,参数23表示开始、结束位置,参数4 SPAN_EXCLUSIVE_INCLUSIVE用来对第二个和第三个参数进一步限制和说明,此处表示不包含1,但是包含3,从字面意思也很好理解。AbsoluteSizeSpan显然就是用来设置字号的,参数1单位为像素值,需要自己转换一下,其他参数含义和ForegroundColorSpan一致。为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为SEU_Calvin的博客


至此关于布局优化的一些小技巧就介绍到这了,希望对你有用~

也希望看官老爷们多点赞支持~

作者:SEU_Calvin 发表于2016/11/28 21:56:08 原文链接
阅读:23 评论:0 查看评论

Android系统源码阅读(14):Zygote和System进程的启动

$
0
0

Android系统源码阅读(14):Zygote和System进程的启动

再不学习我们就老了


0. Zygote有什么卵用?

Zygote是进程孵化器,Android系统中其他服务进程都是拷贝于它。Zygote在设计模式中对应于prototype,这样做的好处是可以通过拷贝Zygote来快速创建一个进程。

1. Zygote脚本启动

在开机时,init进程会调用如下脚本启动进程。

system/core/rootdir/init.zygote32_64.rc :

service zygote /system/bin/app_process32 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

service表明该进程是作为一个服务来启动的,--start-system-server指明了该进程启动后,需要启动system服务。该进程对应的端口权限660,名字为zygote,其它进程可以通过该端口和它进行通信。

1.1 init进程创建新的app_process

在init进程中,启动service进程的过程如下。

system/core/init/init.cpp :

void service_start(struct service *svc, const char *dynamic_args)
    //...
    NOTICE("Starting service '%s'...\n", svc->name);
    //创建新进程
    pid_t pid = fork();
    //在新建的进程中
    if (pid == 0) {
        struct socketinfo *si;
        struct svcenvinfo *ei;
        char tmp[32];
        int fd, sz;
        //依次创建service中的socket
        for (si = svc->sockets; si; si = si->next) {
            int socket_type = (
                    !strcmp(si->type, "stream") ? SOCK_STREAM :
                        (!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));
            //创建socket
            int s = create_socket(si->name, socket_type,
                                  si->perm, si->uid, si->gid, si->socketcon ?: scon);
            if (s >= 0) {
                //发布socket
                publish_socket(si->name, s);
            }
        }

    //...
    //将参数拷贝进svc结构体中
        if (!dynamic_args) {
            //没有参数的情况
            //svc->args[0]对应于/system/bin/app_process32
            //下一步会加载该程序,并且传入参数
            if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) {
                ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno));
            }
        } else {
            char *arg_ptrs[INIT_PARSER_MAXARGS+1];
            int arg_idx = svc->nargs;
            char *tmp = strdup(dynamic_args);
            char *next = tmp;
            char *bword;

            /* Copy the static arguments */
            memcpy(arg_ptrs, svc->args, (svc->nargs * sizeof(char *)));

            while((bword = strsep(&next, " "))) {
                arg_ptrs[arg_idx++] = bword;
                if (arg_idx == INIT_PARSER_MAXARGS)
                    break;
            }
            arg_ptrs[arg_idx] = NULL;
            execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);
        }
        _exit(127);
    }
    //...
}

1.2 socket创建和发布

下面主要分析一下create_socketpublish_socket两个函数,来说明zygote的socket如何创建的。

system/core/init/util.cpp :

/*
 * create_socket - creates a Unix domain socket in ANDROID_SOCKET_DIR
 * ("/dev/socket") as dictated in init.rc. This socket is inherited by the
 * daemon. We communicate the file descriptor's value via the environment
 * variable ANDROID_SOCKET_ENV_PREFIX<name> ("ANDROID_SOCKET_foo").
 */
int create_socket(const char *name, int type, mode_t perm, uid_t uid,
                  gid_t gid, const char *socketcon)
{
    struct sockaddr_un addr;
    int fd, ret;
    char *filecon;

    if (socketcon)
        setsockcreatecon(socketcon);
    //创建一个socket
    fd = socket(PF_UNIX, type, 0);
    if (fd < 0) {
        ERROR("Failed to open socket '%s': %s\n", name, strerror(errno));
        return -1;
    }

    if (socketcon)
        setsockcreatecon(NULL);

    //创建一个socket地址addr
    memset(&addr, 0 , sizeof(addr));
    addr.sun_family = AF_UNIX;
    //设置地址的文件位置,这里就是/dev/socket/zygote
    snprintf(addr.sun_path, sizeof(addr.sun_path), ANDROID_SOCKET_DIR"/%s",
             name);

    ret = unlink(addr.sun_path);
    if (ret != 0 && errno != ENOENT) {
        ERROR("Failed to unlink old socket '%s': %s\n", name, strerror(errno));
        goto out_close;
    }

    filecon = NULL;
    if (sehandle) {
        ret = selabel_lookup(sehandle, &filecon, addr.sun_path, S_IFSOCK);
        if (ret == 0)
            setfscreatecon(filecon);
    }

    //将想要存储socket的文件地址addr和socket文件描述符fd绑定起来
    ret = bind(fd, (struct sockaddr *) &addr, sizeof (addr));
    if (ret) {
        ERROR("Failed to bind socket '%s': %s\n", name, strerror(errno));
        goto out_unlink;
    }

    setfscreatecon(NULL);
    freecon(filecon);

    //设置用户组root system
    chown(addr.sun_path, uid, gid);
    //设置权限660
    chmod(addr.sun_path, perm);

    INFO("Created socket '%s' with mode '%o', user '%d', group '%d'\n",
         addr.sun_path, perm, uid, gid);

    return fd;

out_unlink:
    unlink(addr.sun_path);
out_close:
    close(fd);
    return -1;
}
static void publish_socket(const char *name, int fd)
{    
    //前缀为ANDROID_SOCKET_
    char key[64] = ANDROID_SOCKET_ENV_PREFIX;
    char val[64];
    //拼接出key
    strlcpy(key + sizeof(ANDROID_SOCKET_ENV_PREFIX) - 1,
            name,
            sizeof(key) - sizeof(ANDROID_SOCKET_ENV_PREFIX));
    //将fd写入val
    snprintf(val, sizeof(val), "%d", fd);
    //将key,value写入环境变量中,以便其他进程访问
    add_environment(key, val);

    /* make sure we don't close-on-exec */
    fcntl(fd, F_SETFD, 0);
}

2. Zygote进程启动过程

Zygote 进程的启动从app_process的main函数开始。

2.1 app_process.main

判断需要启动的进程的种类。

frameworks/base/cmds/app_process/app_main.cpp :

int main(int argc, char* const argv[])
{
    //针对旧内核做的处理...

    //创建AppRuntime对象
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    // Process command line arguments
    // ignore argv[0]
    argc--;
    argv++;

    // Everything up to '--' or first non '-' arg goes to the vm.
    //
    // The first argument after the VM args is the "parent dir", which
    // is currently unused.
    //
    // After the parent dir, we expect one or more the following internal
    // arguments :
    // 不同的进程类型
    // --zygote : Start in zygote mode
    // --start-system-server : Start the system server.
    // --application : Start in application (stand alone, non zygote) mode.
    // --nice-name : The nice name for this process.
    //
    // For non zygote starts, these arguments will be followed by
    // the main class name. All remaining arguments are passed to
    // the main method of this class.
    //
    // For zygote starts, all remaining arguments are passed to the zygote.
    // main function.
    //
    // Note that we must copy argument string values since we will rewrite the
    // entire argument block when we apply the nice name to argv0.

    int i;
    for (i = 0; i < argc; i++) {
        if (argv[i][0] != '-') {
            break;
        }
        if (argv[i][1] == '-' && argv[i][2] == 0) {
            ++i; // Skip --.
            break;
        }
        runtime.addOption(strdup(argv[i]));
    }

    // Parse runtime arguments.  Stop at first unrecognized option.
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    //判断需要创建何种类型的进程
    ++i;  // Skip unused "parent dir" argument.
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
        //这里是Zygote进程
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            //同时需要开启SystemServer
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }

    Vector<String8> args;
    if (!className.isEmpty()) {
        // We're not in zygote mode, the only argument we need to pass
        // to RuntimeInit is the application argument.
        //
        // The Remainder of args get passed to startup class main(). Make
        // copies of them before we overwrite them with the process name.
        args.add(application ? String8("application") : String8("tool"));
        runtime.setClassNameAndArgs(className, argc - i, argv + i);
    } else {
        // We're in zygote mode.
        maybeCreateDalvikCache();

        if (startSystemServer) {
        //作为参数传递给Zygote进程
            args.add(String8("start-system-server"));
        }

    //... 

        String8 abiFlag("--abi-list=");
        abiFlag.append(prop);
        args.add(abiFlag);

        // In zygote mode, pass all remaining arguments to the zygote
        // main() method.
        for (; i < argc; ++i) {
            args.add(String8(argv[i]));
        }
    }

    if (!niceName.isEmpty()) {
    //这里进程名字就是zygote
        runtime.setArgv0(niceName.string());
        set_process_name(niceName.string());
    }

    if (zygote) {
        //启动Zygote,接下来会主要分析start函数
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        return 10;
    }
}

2.2 AndroidRuntime.start

创建虚拟机,运行java函数。

frameworks/base/core/jni/AndroidRuntime.cpp :

/*
 * Start the Android runtime.  This involves starting the virtual machine
 * and calling the "static void main(String[] args)" method in the class
 * named by "className".
 *
 * Passes the main function two arguments, the class name and the specified
 * options string.
 */
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    //创建一个虚拟机实例
    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    onVmCreated(env);

    /*
     * Register android functions.
     */
    //注册JNI方法
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

    /*
     * 将参数转化为java对象
     * We want to call main() with a String array with arguments in it.
     * At present we have two arguments, the class name and an option string.
     * Create an array to hold them.
     */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    for (size_t i = 0; i < options.size(); ++i) {
        jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
        assert(optionsStr != NULL);
        env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
     * Start VM.  This thread becomes the main thread of the VM, and will
     * not return until the VM exits.
     */
    char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        //找到main函数
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            //调用com.android.internal.os.ZygoteInit类的main函数
            //参数放在strArray里
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
    free(slashClassName);

    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}

2.3 ZygoteInit.main

frameworks/base/core/java/com/android/internal/os/Zygoteinit.java :

 public static void main(String argv[]) {
        try {
            //解析参数
            RuntimeInit.enableDdms();
            // Start profiling the zygote initialization.
            SamplingProfilerIntegration.start();

            boolean startSystemServer = false;
            String socketName = "zygote";
            String abiList = null;
            for (int i = 1; i < argv.length; i++) {
                if ("start-system-server".equals(argv[i])) {
                    startSystemServer = true;
                } else if (argv[i].startsWith(ABI_LIST_ARG)) {
                    abiList = argv[i].substring(ABI_LIST_ARG.length());
                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
                    socketName = argv[i].substring(SOCKET_NAME_ARG.length());
                } else {
                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
                }
            }

            if (abiList == null) {
                throw new RuntimeException("No ABI list supplied.");
            }
            //注册Socket,创建一个socket服务端
            registerZygoteSocket(socketName);
            //...

            // Do an initial gc to clean up after startup
            gcAndFinalize();

            // Disable tracing so that forked processes do not inherit stale tracing tags from
            // Zygote.
            Trace.setTracingEnabled(false);

            if (startSystemServer) {
                //启动系统服务
                startSystemServer(abiList, socketName);
            }

            Log.i(TAG, "Accepting command socket connections");
            //循环等待其他服务向zygote socket发送请求
            runSelectLoop(abiList);

            closeServerSocket();
        } catch (MethodAndArgsCaller caller) {
            caller.run();
        } catch (RuntimeException ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }
作者:tianchi92 发表于2016/11/28 22:06:45 原文链接
阅读:16 评论:0 查看评论

Unity3D开发小贴士(十四)JsonUtility

$
0
0

Json是现在非常常用的数据格式,因为.Net的版本问题,所有没有很方便的方法可以直接在Unity里面使用C#官方的Json库,于是Unity3D自己提供了自己的一套Json工具——JsonUtility。

参考下面的示例:

using UnityEngine;
using System.Collections;

public class TestSer
{
	public int i;
	public string str;
}

public class TestDeser
{
	public string str;
	public float i;
}
public class TestDeser2
{
	public string st;
	public float f;
}

public class NewBehaviourScript : MonoBehaviour {

	// Use this for initialization
	void Start () {
		TestSer ts = new TestSer();
		ts.i = 10;
		ts.str = "aaaa";
		string json = JsonUtility.ToJson (ts);
		Debug.Log (json); 
		TestDeser td = JsonUtility.FromJson<TestDeser> (json);
		Debug.Log (td.str);
		Debug.Log (td.i);
		TestDeser2 td2 = JsonUtility.FromJson<TestDeser2> (json);
		Debug.Log (td2.st);
		Debug.Log (td2.f);
	}
}


可以看出,JsonUtility使用起来很方便,而且没什么节操约束。

JsonUtility支持数组,并且支持可序列化的自定义类型,也就是需要添加System.Serializable特性(关于特性,参考C#语法小知识(七)特性)。

例如:

[System.Serializable]
public struct TestObj
{
	public string ttt;
}

public class TestSer2
{
	public int i;
	public string str;
	public TestObj obj;
	public int[] arr;
	public GameObject go;
}

序列化:

		TestSer2 ts2 = new TestSer2();
		ts2.i = 10;
		ts2.str = "aaaa";
		ts2.obj = new TestObj ();
		ts2.obj.ttt = "tttt";
		ts2.arr = new int[3];
		ts2.arr [0] = 1;
		ts2.arr [1] = 1;
		ts2.arr [2] = 1;
		ts2.go = new GameObject ("go");
		string json = JsonUtility.ToJson (ts2);

打印出来的结果:

{"i":10,"str":"aaaa","obj":{"ttt":"tttt"},"arr":[1,1,1],"go":{"instanceID":-12766}}

毫无疑问,JsonUtility使用了C#反射(参考C#语法小知识(十)反射),所以效率上会差一点。效率方面,肯定还是微软自家的Binary和XML库会稍微好一点。(详情参考C#语法小知识(十六)序列化与反序列化(XML)C#语法小知识(十七)序列化与反序列化(Binary)




作者:ecidevilin 发表于2016/11/28 22:10:23 原文链接
阅读:38 评论:0 查看评论

Android的Ui层次

$
0
0

UI 概览

Android 应用中的所有用户界面元素都是使用 View 和 ViewGroup 对象构建而成。View 对象用于在屏幕上绘制可供用户交互的内容。ViewGroup 对象用于储存其他 View(和 ViewGroup)对象,以便定义界面的布局。

Android 提供了一系列 View 和 ViewGroup 子类,可为您提供常用输入控件(如按钮和文本字段)和各种布局模式(如线性布局或相对布局)。

用户界面布局

如图 所示,每个应用组件的用户界面都是使用 View 和 ViewGroup 对象的层次结构定义的。每个视图组都是一个用于组织子视图的不可见容器,而子视图可以是输入控件或其他可绘制某一 UI 部分的小部件。 此层次结构树可繁可简,随需而定(但是简单的结构可提供最佳性能)。
这里写图片描述

图 . 视图层次结构的图示,它定义了一个 UI 布局。

要声明布局,您可以实例化代码中的 View 对象并开始构建树,但是定义布局最简单且最有效的方法是使用 XML 文件。如同 HTML 一样,XML 也为布局提供了一种用户可读结构。

视图的 XML 元素名称与其代表的 Android 类相对应。因此, TextView 元素用于在 UI 中创建一个 TextView 小部件,LinearLayout 元素用于创建一个 LinearLayout 视图组。

例如,包含文本视图和按钮的简单垂直布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="I am a TextView" />
    <Button android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="I am a Button" />
</LinearLayout>

在应用中加载布局资源时,Android 会将布局的每个节点初始化为运行时对象,供您定义其他行为、查询对象状态或修改布局。

我的微信二维码如下,欢迎交流讨论

这里写图片描述

欢迎关注《IT面试题汇总》微信订阅号。每天推送经典面试题和面试心得技巧

微信订阅号二维码如下:

这里写图片描述

作者:u010321471 发表于2016/11/28 22:13:10 原文链接
阅读:99 评论:0 查看评论

Android艺术开发探索第四章——View的工作原理(上)

$
0
0

Android艺术开发探索第四章——View的工作原理(上)


这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来说,View是Android中视觉的呈现,在界面上Android提供了一套完整的GUI库,里面有很多控件,但是有时候往往并不能满足于需求,所以只有自定义View了,我们会简单的说下流程,然后再去实践

除了View的三大流程之外,View常见的回调方法也是必须掌握的,比如构造方法,onAttach,onVisibilityChanged,onDetach,另外对于一些有滑动效果的自定义View,还要处理滑动事件和滑动冲突,总的来说,自定义View有几种固定的类型,View或者ViewGroup,有的直接重写原生控件,这个就要看需求了,好的,我们直接开始吧!

一.初识ViewRoot和DecorView

在正式介绍View的三大流程之前,我们还是要了解一些基本的概念,所以本章会说下ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,他是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的,在ActivityThread中,当Activity被创建完毕后,会将DecorView添加到Window值班费,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立联系,这个可以参照官网:

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);

View的绘制流程从ViewRoot的perfromTraversals方法开始,他警告measure,layout和draw三个过程才能将View画出来,,其中measure测量,layout确定view在容器的位置,draw开始绘制在屏幕上,针对perfromTraversals的大致流程,可以看图

这里写图片描述

图中的perfromTraversals会依次调用perfromMeasure,perfromLayout,perfromDraw,他们分别完成顶级View的measure,layout和draw这三大流程,其中在perfromMeasure中会调用measure方法,在measure方法中又调用onMeasure,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure过程,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历,同理,其他两个也是如此,唯一有点区别的是perfromDraw的传递过程是在draw反复中通过dispatchDraw来实现的,不过这并没有什么本质的区别

measure過程决定了View的宽高,Measure完成之后可以通过getMeasureWidth和getMeasureHeight来获取View测量后的高宽,在所有的情况下塔几乎都是等于最终的宽高,但是特殊情况除外,这点以后说,layout过程决定了view的四个顶点的坐标和实际View的宽高,完成之后,通过getTop,getLeft,getRight,getBottom获得,,Draw决定了View的显示,只有draw方法完成了之后,view才会显示在屏幕上

如下图,顶级View DecorView,一般情况下他内部会包含一个竖直方向的LinearLayout,这里面有上下两部分,上面是标题栏,下面是内容,在Activity中,我们可用通过setContentView设置布局文件就是放在内容里,而内容栏的id为content,因此我們可以理解为实际上是在setView,那如何得到content呢?你可以ViewGroup content = findviewbyid(android.R.id.content),如何得到我们设置的View呢:content.getChildAt(0),同时,通过源码我们可用知道,DeaorView其实就是一个FrameLayout,View层事件都先经过DecorView,然后传递给View

这里写图片描述

二.理解MeasureSpec

为了更好的理解View的测量过程,我们还需要理解MeasureSpec,从名字上看,MeasureSpec看起来像“测量规格”或者“测量说明书”,不管怎么翻译,他看起来就好像是或多或少的决定了View的测量过程,通过源码可以发现,MeasureSpec的确参与了View的测量过程,读者可能有疑问,MeasureSpec是干什么的呢?MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说很大程度上是因为这个过程还收到了父容器的影像,因为父容器影像MeasureSpec的创建过程,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高,MeasureSpec看起来有点复杂,其实他的实现很简单,我们来详细分解一下

1.MeasureSpec

MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某个测量模式下的规格大小,下面先看一下,MeasureSpec内部的一些常量定义,通过这些就不难理解MeasureSpec的工作原理了

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

     public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的作用,SpecMode和specSize也是一个int值,一直SpecMode和specSize可以打包成一个MeasureSpec,一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而非MeasureSpec本身。

SpecMode有三类,每一类都有特殊的含义

  • UNSPECIFIED

父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态

  • EXACTLY

父容器已经检测出View所需要的精度大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent,和具体的数值这两种模式

  • AT_MOST

父容器指定了一个可用大小,即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中wrap_content

2.MeasureSpec 和 LayoutParams 的对应关系

系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View的测量,但是正常情况下我们使用View指定MeasureSpec,但是尽管如此,我们也可以给View设置layoutparams,在view测量的时候,系统会将layoutparams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定view测量后的宽高,需要注意的是,MeasureSpec不是唯一由layoutparams决定的,layoutparams需要和父容器一起决定view的MeasureSpec从而进一步决定view的宽高,对于顶级view(DecorView)和普通的view来说,MeasureSpec的转换过程有些不同,对于decorview,其MeasureSpec由父容器的MeasureSpec和自身的layoutparams来决定,MeasureSpec一旦确定后,MeasureSpec就可以去为view测量了

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有这么一段代码。他展示了DecorViwew的MeasureSpec创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸

 childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
 childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接下来看下getRootMeasureSpec方法的实现:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

通过上述代码,DecorView的MesourSpec的产生过程就很明确了,具体来说其遵守了如下格式,根据layoutparams的宽高的参数来划分

  • LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小

对于普通的View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargis方法

     protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述的方法会对子元素进行measure,在调用子元素的measure方法之前会通过getChildMeasureSpec方法得到子元素的MesureSpec,从代码上看,很显然,子元素的MesureSpec的创建和父容器的MesureSpec和子元素的LayoutParams有关,此外,还和view的margin有关,具体可以看下ViewGroup的getChildMeasureSpec方法

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上述方法不难理解,他的主要作用是根据父容器的MeasureSpec同时结合view本身来layoutparams来确定子元素的MesureSpec,参数中的pading是指父容器中已占有的控件大小,因此子元素可以用的大小为父容器的尺寸减去pading,具体代码

int specSize = MesureSpec.getSize(spec);
int size = Math.max(0,specSize - pading);

getChildMeasureSpec清楚的展示了普通View的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeaureSpec的创建规则,更加清晰的理解getChildMeasureSpec的逻辑,这里提供一个表,表中对getChildMeasureSpec的工作原理进行了梳理,表中的parentSize是指父容器中目前可使用的大小:

这张表暂时不画,可以到书中看 182页

针对这张表,这里再做一下说明。前面已经提到,对于普通View,其MeasureSpec 由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和Viev本身不同的LayoutParams,View就可以有多种MeasureSpec。这里简单说一下,当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View 的MeasureSpee都是精确模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化,并且大小不能超过父容器的剩余空间,可能读者会发现,在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。

通过这张表可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出子元素的MeasureSpec了,有了 MeasureSpec就可以进一步确定出子元亲测量后的大小了。需要说明的是,表中并非是什么经验总结,它只是getcchildMeasureSpec
这个方法以表格的方式呈现出来而已

3.View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draww则将View绘制到屏幕上。

1.measure过程

measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就可以完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论

1.View的measure过程

View 的 measure过程由其measure方法来完成,measure方法是一个final类型的方法,这就意味着子类不能重写此方法,在View的measure方法中去调用View的onMesure方法,因此只需要看onMeasure的实现即可,View的onMesure方法如下所示:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

上面的代码很简介,但是简洁不代表简单,setMeasuredDimension会设置View宽/高的测量值,因此我们只需要getDefaultSize方法即可。

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

可以看出,getDefaultSize这个逻辑很简单,对于我们来说,我们只需要看AT_MOST和EXACTLY这两种情况,简单的理解,其实getDefaultSize返回的大小就是mesourSpec中的specSize,而这个specSize就是view的大小,这里多次提到测量后的大小,是因为View最终的大小,是在layout阶段的,所以这里必须要加以区分,但是几乎所有情况下的View的测量大小和最终大小是相等的

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数是size,即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight()这两个方法的返回值:

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

这里只分析getSuggestedMinimumWidth方法的实现,getSuggestedMinimumHeight和他的原理是一样的。从 getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,View的宽度为mMinwidth,而mMinwidth对应于android:minwidth这个属性所指定的值,因此View的宽度即为android:minwidth属性所指定的值。这个属性如果不指定,那么MinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinwidth
mbackground().getMininumwidth),mMinwidthh的含义我们已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下Drwable的 getMinimumWidth方法,如下所示:

 public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。那么Drawable在什么情况下有原始宽度呢?这里先举个例子说明一下,ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸),详细内容会在第6章进行介绍。

这里再总结一下getSuggestedMinimumWidth的逻辑:如果View没有设置背景,那么返回android:minwidth这个属性所指定的值,这个值可以为0:如果View设置了背景,则返回 android:minwidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View 在UNSPECIFIED情况下的测量宽/高。

从getDefaulSize方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrapcontent时的自身大小,否则在布局中使用wrap_content就相当于使用matchparent。为什么呢?这个原因需要结合上述代码和之前的表才能更好地理解。从上述代码中我们知道,如果View在布局中使用wrapcontent,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于 specSize;查表4-1可知,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。如何解决这个问题呢?也很简单,代码如下所示。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (eightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

在上面的代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight)),并在wrapcontent时设置此宽/高即可。对于非wrapcontent情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看TextView、Imageview等的源码就可以知道,针对 wrapcontent情形,它们的onMeasure方法均做了特殊处理,读者可以自行查看它们的源码。

2.ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再通归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren:

   protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

从上述代码中看到,在ViewGroup的measure时,会对每一个子元素进行测量,那么这个方法就很好理解了

 protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

很显然,measurechild的思想就是取出子元素的LayoutParams,然后再通过getChidMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getchildMeasureSpec的工作过程已经在上面进行了详细分析。

我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout,RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,比如Lineartayout和RelativeL.ayout这两者的布局特性显然不同,因此ViewGroup无法做统
一实现。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的 measure过程,其他Layout类型读者可以自行分析。

首先,我们来看一下LinearLayout的onMeasure方法

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

上述的代码很简单我们选择一个来看下,比如选中竖直方向的LinearLayout测量过程,即measureVertical,他的源码还比较长,我们看:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int childState = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;        
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
        int consumedExcessSpace = 0;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);

从上面的代码可以看出,系统会遍历子元素并对每一个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统通过mTotalLength这个变量来存储LinearLayout在竖直方向上的初步高度,没测量一个子元素,mTotalLength就会增加,增加的部分主要包括子元素的高度以及竖直方向上的margin等,当子元素测量完毕之后,LinearLayout会测量自己的大小,看源码:

// Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
        if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final float childWeight = lp.weight;
                if (childWeight > 0) {
                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                    remainingExcess -= share;
                    remainingWeightSum -= childWeight;

                    final int childHeight;
                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                        childHeight = largestChildHeight;
                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                            || heightMode == MeasureSpec.EXACTLY)) {
                        // This child needs to be laid out from scratch using
                        // only its share of excess space.
                        childHeight = share;
                    } else {
                        // This child had some intrinsic height to which we
                        // need to add its share of excess space.
                        childHeight = child.getMeasuredHeight() + share;
                    }

                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width);
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);


            // We have no limit, so make all weighted views as tall as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }

                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

这里对上述代码进行说明,当子元素测量完毕之后,LinearLayout会根据子元素的情况来测量自己的大小,针对竖直的LinearLayout而言,他的水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程和View有些不同,具体来说,是指,如果他的布局中高度采用的是match_parent或者具体值,那么他的绘制过程和View一致,即高度为specSize,如果他的布局中高度采用warp_content,那么她的高度是所有的子元素所占用的高度综合,但是仍然不能超过他的父容器剩余空间,但是他的最终高度还是需要考虑其他的竖直方向上的pading,这个过程进一步参看源码:

 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

View的onMeasure是三大流程中最复杂的一个,measure完成以后,通过getMeasureWidth/Height就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下measure才能确定最终的测量宽/高,在这种情形下,系统可能要多次调用measure方法进行测量,在这种情况下,载onMeasure方法中拿到的测量值很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

上面已经对Viaw的measure过程进行了详细的分析,现在考虑一种情况,比如我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高,读者可能会说,这很简单啊,在onCreate或者onResume里面去获取这个View的宽/高就行了,读者可以自行试一下,实际上在onCreate、onStart、onResume中均无法正确得View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activiy执行了onCreate、onStart、onResume时某个Vicw已经完毕了,如果View还没有测量完毕,那么获得的宽/高就是0。有没有什么方法能解决问题呢?答案是有的,这里给出四种方法来解决这个问题:

  • (1)Activity/View#onWindowFocusChanged。

onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。典型代码如下:

 public void onWindowFocusChanged(boolean hasWindowFocus) {
        InputMethodManager imm = InputMethodManager.peekInstance();
        if (!hasWindowFocus) {
            if (isPressed()) {
                setPressed(false);
            }
            if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
                imm.focusOut(this);
            }
            removeLongPressCallback();
            removeTapCallback();
            onFocusLost();
        } else if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
            imm.focusIn(this);
        }
        refreshDrawableState();
    }
  • (2)view.post(runnable)

通过post可以将一个runnable投递到消息队列,然后等到Lopper调用runnable的时候,View也就初始化好了,典型代码如下:

    @Override
    protected void onStart() {
        super.onStart();

        mTextView.post(new Runnable() {
            @Override
            public void run() {
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }
  • (3)ViewTreeObserver

使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变,onGlobalLayout方法就会回调,因此这是获取View的宽高一个很好的例子,需要注意的是,伴随着View树状态的改变,这个方法也会被调用多次,典型代码如下

    @Override
    protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = mTextView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }
  • (4)view.measure(int widthMeasureSpec , int heightMeasureSpec)

通过手动测量View的宽高,这种方法比较复杂,这里要分情况来处理,根据View的LayoutParams来处理

  • match_parent

直接放弃,无法测量出具体的宽高,根据View的测量过程,构造这种measureSpec需要知道parentSize,即父容器的剩下空间,而这个时候我们无法知道parentSize的大小,所以理论上我们不可能测量出View的大小

  • 具体的数值

比如宽高都是100dp,那我们可以这样:

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        mTextView.measure(widthMeasureSpec,heightMeasureSpec);
  • warap_content

如下measure

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        mTextView.measure(widthMeasureSpec,heightMeasureSpec);

注意到(1<<30)-1, 通过分析MeasureSpec的实现可以知道,View的尺寸三十位的二进制表示,也就是说最大是30个1(2^30-1),也就是(1<30-1),在最大的模式下,我们用View理论上能支持最大值去构造MwasureSpec是合理的

关于View的measure,网络上有两个错误的用法,为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的MeasureSpec去得出合理的SpecMode,从而导致measure过程出错,其次不能保证mwasure出正确的结果)

  • 第一种错误的方法:
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
        mTextView.measure(widthMeasureSpec,heightMeasureSpec);
  • 第二种错误的用法
mTextView.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);

2.layout过程

Layout的作用是ViewGroup用来确定子元素的作用的,当ViewGroup的位置被确认之后,他的layout就会去遍历所有子元素并且调用onLayout方法,在layout方法中onLayou又被调用,layout的过程和measure过程相比就要简单很多了,layout方法确定了View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

layout的方法的大致流程如下,首先会通过一个setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mTop,mRight,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了,接下来会调用onLayout方法,这个方法的用途是调用父容器确定子元素的位置,和onMeasure类似,onLayout的具体位置实现同样和具体布局有关,所有View和ViewGroup均没有真正的实现onLayout方法,我们来看一下LinearLayout的onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

很好理解,是吧,,这个和onMeasure有点类似,我们拿layoutVertical来说,先看源码:

   void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐变大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合树立方向的线性布局,至于setChildFrame,他仅仅是调用元素的layout方法而已,这样的父容器在layout方法中完成自己的定位以后,就通过onLayout方法去调用,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层传递下去完成整个View树的layout过程,setChildFrame方法可以看:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

我们注意到setChildFrame中的width和height实际上就是子元素测量宽高,从下面的代码可以看出

final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

而在Layout方法中通过setFrame去设置子元素的四个顶点位置,方法中有这么几句:

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;

下面我们来回到之前的问题,View的测量宽高和最终宽高有什么区别,这个问题现在可以具体回答了,View的getMeasureWidth和getWidth这两个方法有什么区别?至于getMeasureHeight和getHeight是完全一样的,为了回答这个问题我们首先来看下getWidth和getHeight具体实现

    public final int getWidth() {
        return mRight - mLeft;
    }

        public final int getHeight() {
        return mBottom - mTop;
    }

从getWidth和getHeight的源码在结合这四个变量的赋值来看,getWidth返回的刚好是View测量的值,而getHeight也是一样的,所以我们可用回答上面的问题了:在View的默认实现中,View的测量宽高和最终的是一样的,只不过一个是measure过程,一个是layout过程,而最终形成的是layout过程,即两者的赋值时机不同,测量宽高的赋值时机,稍微早一些,因此,在日常开发中,我们可用认为他们是相等的,但是还是有些不相同的,我们可用重写View的layout方法

    public void layout(int l,int t,int r, int b){
        super.layout(l,t,t+100,b+100);
    }

上述代码会导致在任何平台下View的最终宽高总是比测量大于100px,虽然这样这样会导致View显示不正常和没什么意义,但是这证明了测量 不等于 最终,另一种情况是在某种情况下,View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高是不一致的但最终是一致的

3.draw过程

Drawable是比較简单的,他的作用是将View绘制到屏幕上面,View的绘制过程由如下几个步骤:

  • 1.绘制背景
  • 2.绘制自己
  • 3.绘制children
  • 4.绘制装饰

这一点我们看他的源码就知道了

   public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

从setwilINotDraw这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个校化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是。当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。

好的,这既时View工作原理的上半部分,太多了剩下的我们下半部分再说

PPT:下篇提供

Sample:下篇提供

MakeDown:http://pan.baidu.com/s/1o7Z4Djs 密码:xdgt

通往Android的神奇之旅:555974449

作者:qq_26787115 发表于2016/11/28 22:24:42 原文链接
阅读:67 评论:0 查看评论

USB驱动程序之USB设备驱动程序1简单编写

$
0
0

1、驱动编写分析

(1)usb总线驱动程序在我们接入USB设备的时候会帮我们构造一个新的usb_device.注册到总线里面来。左边这一块已经帮我们做好了,我们要做的是右边这一块。我们要构造一个usb_driver结构体,然后注册进去。usb_driver结构体里面的id_table表示能够支持哪一些设备,里面的probe函数表示能够支持接入设备时会被调用。

(2)目的

USB鼠标用作键盘。左键按下时相当于字母L,右键按下时相当于字母S,鼠标中间的滑轮相当于回车。

(3)输入子系统

在probe函数里面做以下几件事情

  • 分配input_dev结构体
  • 设置它能产生按键类事件
  • 注册
  • 硬件相关的操作(对于usb鼠标来说,使用usb总线驱动程序提供的读写函数来收发数据)
(4)usb驱动程序编写

  • 分配/设置usb_driver结构体
  •         .id_table
            .probe
            .disconnect
  • 注册

2、参考usbmouse.c驱动

(1)usb_driver结构体


(2)id_table


打开宏(match_flag是表示匹配(设备描述符)的哪一项)int是interface的简写,匹配接口的信息(接口描述符),只要接口描述符里面的接口的类是cl,子类是sc,协议是pr,就能够支持。只要USB设备的接口描述符里面的类是HID类,子类是BOOT,协议是MOUSE,就能够支持。


(3)接口描述符(代表逻辑设备)


(4)(id_table)想支持某一款设备,厂家ID和设备ID


下面可以自己设备厂家ID和设备ID


###################################################

##################################################3

(5)probe函数


参数usb_interface结构体,一个usb硬件设备有多个逻辑上的设备,逻辑上的设备使用usb_interface结构体表示的,

参考http://blog.csdn.net/qingkongyeyue/article/details/53363298



编译驱动程序

拷贝别的文件的Makefile,然后修改




用make命令进行编译


把生成的驱动程序拷贝到网络文件系统


使用新内核启动开发板(reboot命令重启)


挂接网络文件系统,安装驱动


接上usb鼠标(found usbmouse是在probe函数设置的)


想插入USB时把厂家ID和设备ID打印出来,因为厂家ID和设备ID都是在设备描述符里面的,当我们接入usb设备后,usb总线驱动程序把相关的描述符都读出来啦!我们只要用就可以啦!

在usb_device结构体里面有设备描述符,设备描述符里面就有厂家ID和设备ID、版本号。


卸载原来的驱动装载新驱动



编写驱动usbmouse_as_key.c(参考usbmouse.c)

作者:qingkongyeyue 发表于2016/11/28 22:28:38 原文链接
阅读:38 评论:0 查看评论

Andorid知识点之三十三:Android二维码(仿微信,轻量Zxing)

$
0
0

前言

要做一个功能,二维码识别。网上找一堆相关的Demo,但是总不是想要的效果,或者都是多年前的版本,权衡考虑之后,决定亲自操刀。不纠结直接选中Zxing框架,https://github.com/zxing/zxing 在网站上直接clone下来,运行,然后就发现问题了...

选Zxing存在的问题

  • 为什么是横屏,调成竖屏,居然有问题
  • 这个包居然有好多用不着的代码
  • 默认识别的界面不是想要的效果
  • 加个Title在顶部之后识别框居然不居中

发现问题,那么本文的优点就来了,且听一一道来

  • 集成速度快,相关核心功能都已经再次封装好
  • 最新的V3.30的工程,识别速度快,基本见图秒识别
  • 解决横竖屏的问题,通过设置Activity的android:screenOrientation="portrait" 方式设置,就可以自适应横屏竖屏
  • 去掉工程中无用的代码,留下最核心的代码,实现最最最轻量级
  • 自定义AutoScannerView控件,实现微信识别区域的效果
  • 解决工程之间只以屏幕为居中的问题,目前可以根据设置宽度高度自适应居中
  • 具体示例参考:https://github.com/yangxixi88/ZxingLite

光说不练假把式,上动图


模仿微信的效果,真机上效果更好,录屏的将就着看

默认效果

集成方式

1、导入zxinglite工程,Andorid Studio通过Import Module方式导入
2、导入目标工程之后,如果有存在R等资源文件未找到,可以在菜单栏Build->Make Module zxinglite 即可
3、添加两个权限

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

4、集成BaseCaptureActivity,实现getSurfaceView()和dealDecode()等方法
5、布局样式,仿微信效果则用AutoScannerView,默认效果使用com.google.zxing.client.android.ViewfinderView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_wechat_capture"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="yangxixi.zxinglib.WeChatCaptureActivity">

    <SurfaceView
        android:id="@+id/preview_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

    <com.google.zxing.client.android.AutoScannerView
        android:id="@+id/autoscanner_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

6、仿微信效果的Activity

/**
 * 模仿微信的扫描界面
 */
public class WeChatCaptureActivity extends BaseCaptureActivity {

    private static final String TAG = WeChatCaptureActivity.class.getSimpleName();

    private SurfaceView surfaceView;
    private AutoScannerView autoScannerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_wechat_capture);
        surfaceView = (SurfaceView) findViewById(R.id.preview_view);
        autoScannerView = (AutoScannerView) findViewById(R.id.autoscanner_view);
    }

    @Override
    protected void onResume() {
        super.onResume();
        autoScannerView.setCameraManager(cameraManager);
    }

    @Override
    public SurfaceView getSurfaceView() {
        return (surfaceView == null) ? (SurfaceView) findViewById(R.id.preview_view) : surfaceView;
    }

    @Override
    public void dealDecode(Result rawResult, Bitmap barcode, float scaleFactor) {
        Log.i(TAG, "dealDecode ~~~~~ " + rawResult.getText() + " " + barcode + " " + scaleFactor);
        playBeepSoundAndVibrate(true, false);
        Toast.makeText(this, rawResult.getText(), Toast.LENGTH_LONG).show();
//        对此次扫描结果不满意可以调用
//        reScan();
    }
}

默认效果Activity

/**
 * 默认的扫描界面
 */
public class DefaultCaptureActivity extends BaseCaptureActivity {

    private static final String TAG = DefaultCaptureActivity.class.getSimpleName();

    private SurfaceView surfaceView;
    private ViewfinderView viewfinderView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_capture);
        surfaceView = (SurfaceView) findViewById(R.id.preview_view);
        viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
    }

    @Override
    public SurfaceView getSurfaceView() {
        return (surfaceView == null) ? (SurfaceView) findViewById(R.id.preview_view) : surfaceView;
    }

    @Override
    public ViewfinderView getViewfinderHolder() {
        return (viewfinderView == null) ? (ViewfinderView) findViewById(R.id.viewfinder_view) : viewfinderView;
    }

    @Override
    public void dealDecode(Result rawResult, Bitmap barcode, float scaleFactor) {
        Log.i(TAG, "dealDecode ~~~~~ " + rawResult.getText() + " " + barcode + " " + scaleFactor);
        playBeepSoundAndVibrate();
        Toast.makeText(this, rawResult.getText(), Toast.LENGTH_LONG).show();
//        对此次扫描结果不满意可以调用
//        reScan();
    }
}

自定义AutoScannerView的实现

/**
 * Created by yangxixi on 16/11/22.
 *
 * 自动上下扫描
 */

public class AutoScannerView extends View {

    private static final String TAG = AutoScannerView.class.getSimpleName();

    private Paint maskPaint;
    private Paint linePaint;
    private Paint traAnglePaint;
    private Paint textPaint;
    private CameraManager cameraManager;

    private final int maskColor = Color.parseColor("#60000000");                          //蒙在摄像头上面区域的半透明颜色
    private final int triAngleColor = Color.parseColor("#76EE00");                        //边角的颜色
    private final int lineColor = Color.parseColor("#FF0000");                            //中间线的颜色
    private final int textColor = Color.parseColor("#CCCCCC");                            //文字的颜色
    private final int triAngleLength = dp2px(20);                                         //每个角的点距离
    private final int triAngleWidth = dp2px(4);                                           //每个角的点宽度
    private final int textMarinTop = dp2px(30);                                           //文字距离识别框的距离
    private int lineOffsetCount = 0;

    public AutoScannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        maskPaint.setColor(maskColor);

        traAnglePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        traAnglePaint.setColor(triAngleColor);
        traAnglePaint.setStrokeWidth(triAngleWidth);
        traAnglePaint.setStyle(Paint.Style.STROKE);

        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setColor(lineColor);

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(textColor);
        textPaint.setTextSize(dp2px(14));
    }

    public void setCameraManager(CameraManager cameraManager) {
        this.cameraManager = cameraManager;
        invalidate();//重新进入可能不刷新,所以调用一次。
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (cameraManager == null)
            return;
        Rect frame = cameraManager.getFramingRect();
        Rect previewFrame = cameraManager.getFramingRectInPreview();
        if (frame == null || previewFrame == null) {
            return;
        }

        int width = canvas.getWidth();
        int height = canvas.getHeight();

        // 除了中间的识别区域,其他区域都将蒙上一层半透明的图层
        canvas.drawRect(0, 0, width, frame.top, maskPaint);
        canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, maskPaint);
        canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, maskPaint);
        canvas.drawRect(0, frame.bottom + 1, width, height, maskPaint);

        String text = "将二维码放入框内,即可自动扫描";
        canvas.drawText(text, (width - textPaint.measureText(text)) / 2, frame.bottom + textMarinTop, textPaint);

        // 四个角落的三角
        Path leftTopPath = new Path();
        leftTopPath.moveTo(frame.left + triAngleLength, frame.top + triAngleWidth / 2);
        leftTopPath.lineTo(frame.left + triAngleWidth / 2, frame.top + triAngleWidth / 2);
        leftTopPath.lineTo(frame.left + triAngleWidth / 2, frame.top + triAngleLength);
        canvas.drawPath(leftTopPath, traAnglePaint);

        Path rightTopPath = new Path();
        rightTopPath.moveTo(frame.right - triAngleLength, frame.top + triAngleWidth / 2);
        rightTopPath.lineTo(frame.right - triAngleWidth / 2, frame.top + triAngleWidth / 2);
        rightTopPath.lineTo(frame.right - triAngleWidth / 2, frame.top + triAngleLength);
        canvas.drawPath(rightTopPath, traAnglePaint);

        Path leftBottomPath = new Path();
        leftBottomPath.moveTo(frame.left + triAngleWidth / 2, frame.bottom - triAngleLength);
        leftBottomPath.lineTo(frame.left + triAngleWidth / 2, frame.bottom - triAngleWidth / 2);
        leftBottomPath.lineTo(frame.left + triAngleLength, frame.bottom - triAngleWidth / 2);
        canvas.drawPath(leftBottomPath, traAnglePaint);

        Path rightBottomPath = new Path();
        rightBottomPath.moveTo(frame.right - triAngleLength, frame.bottom - triAngleWidth / 2);
        rightBottomPath.lineTo(frame.right - triAngleWidth / 2, frame.bottom - triAngleWidth / 2);
        rightBottomPath.lineTo(frame.right - triAngleWidth / 2, frame.bottom - triAngleLength);
        canvas.drawPath(rightBottomPath, traAnglePaint);

        //循环划线,从上到下
        if (lineOffsetCount > frame.bottom - frame.top - dp2px(10)) {
            lineOffsetCount = 0;
        } else {
            lineOffsetCount = lineOffsetCount + 6;
//            canvas.drawLine(frame.left, frame.top + lineOffsetCount, frame.right, frame.top + lineOffsetCount, linePaint);    //画一条红色的线
            Rect lineRect = new Rect();
            lineRect.left = frame.left;
            lineRect.top = frame.top + lineOffsetCount;
            lineRect.right = frame.right;
            lineRect.bottom = frame.top + dp2px(10) + lineOffsetCount;
            canvas.drawBitmap(((BitmapDrawable)(getResources().getDrawable(R.drawable.scanline))).getBitmap(), null, lineRect, linePaint);
        }
        postInvalidateDelayed(10L, frame.left, frame.top, frame.right, frame.bottom);
    }

    private int dp2px(int dp) {
        float density = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f);
    }
}


具体示例,请跳转Github查看,地址:https://github.com/yangxixi88/ZxingLite 

集成之后,基本就可以,如果需要设置横屏竖屏直接设置screenOrientation就好了,里面的效果适配效果都已经实现;直接在dealDecode中处理扫描之后的结果,playBeepSoundAndVibrate可以设置响铃,振动或者同时都可以。想设置微信识别框的参数可以再AutoScannerView中修改,这里就不精细去实现了。

作者:JavaAndroid730 发表于2016/11/28 22:35:03 原文链接
阅读:19 评论:0 查看评论

Android布局概述

$
0
0

布局

布局定义用户界面的视觉结构,如Activity或应用小部件的 UI。您可以通过两种方式声明布局:

在 XML 中声明 UI 元素。Android 提供了对应于 View 类及其子类的简明 XML 词汇,如用于小部件和布局的词汇;
运行时实例化布局元素。您的应用可以通过编程创建 View 对象和 ViewGroup 对象(并操纵其属性)。
Android 框架让您可以灵活地使用以下一种或两种方法来声明和管理应用的 UI。例如,您可以在 XML 中声明应用的默认布局,包括将出现在布局中的屏幕元素及其属性。然后,您可以在应用中添加可在运行时修改屏幕对象(包括那些已在 XML 中声明的对象)状态的代码。

您还应尝试使用层次结构查看器工具来调试布局—当您在模拟器或设备上进行调试时,它会显示布局属性值、绘制具有内边距/外边距指示符的线框以及完整渲染视图。
您可以利用 layoutopt 工具快速分析布局和层次结构中是否存在低效环节或其他问题。
在 XML 中声明 UI 的优点在于,您可以更好地将应用的外观与控制应用行为的代码隔离。您的 UI 描述位于应用代码外部,这意味着您在修改或调整描述时无需修改您的源代码并重新编译。例如,您可以创建适用于不同屏幕方向、不同设备屏幕尺寸和不同语言的 XML 布局。此外,在 XML 中声明布局还能更轻松地显示 UI 的结构,从而简化问题调试过程。因此,本文将侧重于示范如何在 XML 中声明布局。如果您对在运行时实例化 View 对象感兴趣,请参阅 ViewGroup 类和 View 类的参考资料。

一般而言,用于声明 UI 元素的 XML 词汇严格遵循类和方法的结构和命名方式,其中元素名称对应于类名称,属性名称对应于方法。实际上,这种对应关系往往非常直接,让您可以猜到对应于类方法的 XML 属性,或对应于给定 XML 元素的类。但请注意,并非所有词汇都完全相同。在某些情况下,在命名上略有差异。例如,EditText 元素具有的 text 属性对应的类方法是 EditText.setText()。

提示:如需了解有关不同布局类型的更多信息,请参阅常见布局对象。

编写 XML

您可以利用 Android 的 XML 词汇,按照在 HTML 中创建包含一系列嵌套元素的网页的相同方式快速设计 UI 布局及其包含的屏幕元素。

每个布局文件都必须只包含一个根元素,并且该元素必须是视图对象或 ViewGroup 对象。定义根元素之后,即可再以子元素的形式添加其他布局对象或小部件,从而逐步构建定义布局的视图层次结构。例如,以下这个 XML 布局使用垂直 LinearLayout 来储存一个 TextView 和一个 Button:

<?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="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, I am a TextView" />
    <Button android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello, I am a Button" />
</LinearLayout>

在 XML 中声明布局后,请在您的 Android 项目 res/layout/ 目录中以 .xml 扩展名保存文件,以便其能够正确编译。

布局资源文档中提供了有关布局 XML 文件语法的更多信息。

加载 XML 资源

当您编译应用时,每个 XML 布局文件都会编译到一个 View 资源中。 您应该在 Activity.onCreate() 回调实现中从您的应用代码加载布局资源。请通过调用 setContentView(),以 R.layout.layout_file_name 形式向其传递对布局资源的引用来执行此操作。例如,如果您的 XML 布局保存为 main_layout.xml,则需要像下面这样为您的 Activity 加载该布局:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_layout);
}

启动您的 Activity 时,Android 框架会调用 Activity 中的 onCreate() 回调方法(请参阅Activity文档中有关生命周期的阐述)。

属性

每个视图对象和 ViewGroup 对象都支持各自的各类 XML 属性。某些属性是视图对象的专用属性(例如,TextView 支持 textSize 属性),但这些属性也会被任何可以扩展此类的视图对象继承。某些属性通用于所有 View 对象,因为它们继承自根 View 类(如 id 属性)。此外,其他属性被视为“布局参数”,即描述 View 对象特定布局方向的属性,如该对象的父 ViewGroup 对象所定义的属性。

ID

任何视图对象都可能具有关联的整型 ID,此 ID 用于在结构树中对 View 对象进行唯一标识。编译应用后,此 ID 将作为整型数引用,但在布局 XML 文件中,通常会在 id 属性中为该 ID 赋予字符串值。这是所有 View 对象共用的 XML 属性(由 View 类定义),您会经常用到它。XML 标记内部的 ID 语法是:

android:id=”@+id/my_button”
字符串开头处的 @ 符号指示 XML 解析程序应该解析并展开 ID 字符串的其余部分,并将其标识为 ID 资源。加号 (+) 表示这是一个新的资源名称,必须创建该名称并将其添加到我们的资源(在 R.java 文件中)内。Android 框架还提供了许多其他 ID 资源。 引用 Android 资源 ID 时,不需要加号,但必须添加 android 软件包命名空间,如下所示:

android:id=”@android:id/empty”
添加 android 软件包命名空间之后,现在,我们将从 android.R 资源类而非本地资源类引用 ID。

要想创建视图并从应用中引用它们,常见的模式是:

在布局文件中定义一个视图/小部件,并为其分配一个唯一的 ID:

<Button android:id="@+id/my_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/my_button_text"/>

然后创建一个 view 对象实例,并从布局中捕获它(通常使用 onCreate() 方法):
Button myButton = (Button) findViewById(R.id.my_button);
创建 RelativeLayout 时,为 view 对象定义 ID 非常重要。在相对布局中,同级视图可以定义其相对于其他同级视图的布局,同级视图通过唯一的 ID 进行引用。

ID 不需要在整个结构树中具有唯一性,但在您要搜索的结构树部分应具有唯一性(要搜索的部分往往是整个结构树,因此最好尽可能具有全局唯一性)。

布局参数

名为 layout_something 的 XML 布局属性可为视图定义与其所在的 ViewGroup 相适的布局参数。

每个 ViewGroup 类都会实现一个扩展 ViewGroup.LayoutParams 的嵌套类。此子类包含的属性类型会根据需要为视图组的每个子视图定义尺寸和位置。 正如您在图 1 中所见,父视图组为每个子视图(包括子视图组)定义布局参数。
这里写图片描述

图 以可视化方式表示的视图层次结构,其中包含与每个视图关联的布局参数。

请注意,每个 LayoutParams 子类都有自己的值设置语法。 每个子元素都必须定义适合其父元素的 LayoutParams,但父元素也可为其子元素定义不同的 LayoutParams。

所有视图组都包括宽度和高度(layout_width 和 layout_height),并且每个视图都必须定义它们。许多 LayoutParams 还包括可选的外边距和边框。

您可以指定具有确切尺寸的宽度和高度,但您多半不想经常这样做。 在更多的情况下,您会使用以下常量之一来设置宽度或高度:

wrap_content 指示您的视图将其大小调整为内容所需的尺寸。
match_parent 指示您的视图尽可能采用其父视图组所允许的最大尺寸。
一般而言,建议不要使用绝对单位(如像素)来指定布局宽度和高度, 而是使用相对测量单位,如密度无关像素单位 (dp)、wrap_content 或 match_parent,这种方法更好,因为它有助于确保您的应用在各类尺寸的设备屏幕上正确显示。可用资源文档中定义了可接受的测量单位类型。

布局位置

视图的几何形状就是矩形的几何形状。视图具有一个位置(以一对水平向左和垂直向上坐标表示)和两个尺寸(以宽度和高度表示)。 位置和尺寸的单位是像素。

可以通过调用方法 getLeft() 和方法 getTop() 来检索视图的位置。前者会返回表示视图的矩形的水平向左(或称 X 轴) 坐标。后者会返回表示视图的矩形的垂直向上(或称 Y 轴)坐标。 这些方法都会返回视图相对于其父项的位置。 例如,如果 getLeft() 返回 20,则意味着视图位于其直接父项左边缘向右 20 个像素处。

此外,系统还提供了几种便捷方法来避免不必要的计算,即 getRight() 和 getBottom()。 这些方法会返回表示视图的矩形的右边缘和下边缘的坐标。 例如,调用 getRight() 类似于进行以下计算:getLeft() + getWidth()。

尺寸、内边距和外边距

视图的尺寸通过宽度和高度表示。视图实际上具有两对宽度和高度值。

第一对称为测量宽度和测量高度。 这些尺寸定义视图想要在其父项内具有的大小。 这些测量尺寸可以通过调用 getMeasuredWidth() 和 getMeasuredHeight() 来获得。

第二对简称为宽度和高度,有时称为绘制宽度和绘制高度。 这些尺寸定义视图在绘制时和布局后在屏幕上的实际尺寸。 这些值可以(但不必)与测量宽度和测量高度不同。 宽度和高度可以通过调用 getWidth() 和 getHeight() 来获得。

要想测量其尺寸,视图需要将其内边距考虑在内。内边距以视图左侧、顶部、右侧和底部各部分的像素数表示。 内边距可用于以特定数量的像素弥补视图的内容。 例如,左侧内边距为 2,会将视图的内容从左边缘向右推 2 个像素。 可以使用 setPadding(int, int, int, int) 方法设置内边距,并通过调用 getPaddingLeft()、getPaddingTop()、getPaddingRight() 和 getPaddingBottom() 进行查询。

尽管视图可以定义内边距,但它并不支持外边距。 不过,视图组可以提供此类支持。如需了解更多信息,请参阅 ViewGroup 和 ViewGroup.MarginLayoutParams。

常见布局

ViewGroup 类的每个子类都提供了一种独特的方式来显示您在其中嵌套的视图。以下是 Android 平台中内置的一些较为常见的布局类型。

注:尽管您可以通过将一个或多个布局嵌套在另一个布局内来实现您的 UI 设计,但应该使您的布局层次结构尽可能简略。布局的嵌套布局越少,绘制速度越快(扁平的视图层次结构优于深层的视图层次结构)。

线性布局

一种使用单个水平行或垂直行来组织子项的布局。它会在窗口长度超出屏幕长度时创建一个滚动条。

相对布局

让您能够指定子对象彼此之间的相对位置(子对象 A 在子对象 B 左侧)或子对象与父对象的相对位置(与父对象顶部对齐)。

网页视图

显示网页。

使用适配器构建布局
如果布局的内容是属于动态或未预先确定的内容,您可以使用这样一种布局:在运行时通过子类 AdapterView 用视图填充布局。 AdapterView 类的子类使用 Adapter 将数据与其布局绑定。Adapter 充当数据源与 AdapterView 布局之间的中间人—Adapter(从数组或数据库查询等来源)检索数据,并将每个条目转换为可以添加到 AdapterView 布局中的视图。

适配器支持的常见布局包括:

列表视图

显示滚动的单列列表。

网格视图

显示滚动的行列网格。

使用数据填充适配器视图
您可以通过将 AdapterView 实例与 Adapter 绑定来填充 AdapterView(如 ListView 或 GridView),此操作会从外部来源检索数据,并创建表示每个数据条目的 View。

Android 提供了几个 Adapter 子类,用于检索不同种类的数据和构建 AdapterView 的视图。 两种最常见的适配器是:

ArrayAdapter

请在数据源为数组时使用此适配器。默认情况下,ArrayAdapter 会通过在每个项目上调用 toString() 并将内容放入 TextView 来为每个数组项创建视图。
例如,如果您具有想要在 ListView 中显示的字符串数组,请使用构造函数初始化一个新的 ArrayAdapter,为每个字符串和字符串数组指定布局:

ArrayAdapter adapter = new ArrayAdapter(this,
android.R.layout.simple_list_item_1, myStringArray);
此构造函数的参数是:

您的应用 Context
包含数组中每个字符串的 TextView 的布局
字符串数组
然后,只需在您的 ListView 上调用 setAdapter():

ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(adapter);
要想自定义每个项的外观,您可以重写数组中各个对象的 toString() 方法。或者,要想为 TextView 之外的每个项创建视图(例如,如果您想为每个数组项创建一个 ImageView),请扩展 ArrayAdapter 类并重写 getView() 以返回您想要为每个项获取的视图类型。

SimpleCursorAdapter

请在数据来自 Cursor 时使用此适配器。使用 SimpleCursorAdapter 时,您必须指定要为 Cursor 中的每个行使用的布局,以及应该在哪些布局视图中插入 Cursor 中的哪些列。 例如,如果您想创建人员姓名和电话号码列表,则可以执行一个返回 Cursor(包含对应每个人的行,以及对应姓名和号码的列)的查询。 然后,您可以创建一个字符串数组,指定您想要在每个结果的布局中包含 Cursor 中的哪些列,并创建一个整型数组,指定应该将每个列放入的对应视图:
String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER};
int[] toViews = {R.id.display_name, R.id.phone_number};
当您实例化 SimpleCursorAdapter 时,请传递要用于每个结果的布局、包含结果的 Cursor 以及以下两个数组:

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
R.layout.person_name_and_number, cursor, fromColumns, toViews, 0);
ListView listView = getListView();
listView.setAdapter(adapter);
然后,SimpleCursorAdapter 会使用提供的布局,将每个 fromColumns 项插入对应的 toViews 视图,为 Cursor 中的每个行创建一个视图。

如果您在应用的生命周期中更改了适配器读取的底层数据,则应调用 notifyDataSetChanged()。此操作会通知附加的视图,数据发生了变化,它应该自行刷新。

处理点击事件

您可以通过实现 AdapterView.OnItemClickListener 界面来响应 AdapterView 中每一项上的点击事件。 例如:

// Create a message handling object as an anonymous class.
private OnItemClickListener mMessageClickedHandler = new OnItemClickListener() {
public void onItemClick(AdapterView parent, View v, int position, long id) {
// Do something in response to the click
}
};

listView.setOnItemClickListener(mMessageClickedHandler);

我的微信二维码如下,欢迎交流讨论

这里写图片描述

欢迎关注《IT面试题汇总》微信订阅号。每天推送经典面试题和面试心得技巧

微信订阅号二维码如下:

这里写图片描述

作者:u010321471 发表于2016/11/28 22:37:23 原文链接
阅读:91 评论:0 查看评论

自定义控件九宫格滑动解锁

$
0
0

前言

  • 最近想给自己做的的app添加一个滑动解锁的功能,用的是乐视的手机,就模仿它的效果实现.

  • 视频演示一下效果

  • GitHub

LockPoint实体

  • 每个点是一个实体(LockPoint)用来存储这个点的所有信息,包括点的物理位置(x,y)和点的index位置(0-8)
class LockPoint {
        // 点的位置 0-8
        int index;
        // 点的x,y坐标
        float x, y;
        // 构造方法,初始化一个点
        LockPoint(int index, float x, float y) {
            this.index = index;
            this.x = x;
            this.y = y;
        }
        // 构造方法,从另一个点初始化
        LockPoint(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 默认构造方法,初始化为一个空的点
        LockPoint() {
            this.x = -1;
            this.y = -1;
            this.index = -1;
        }
        // 判断该点是不是一个空的点
        boolean isEmpty() {
            return this.x == -1 && this.y == -1;
        }
        // 重新给位置赋值
        void init(float x, float y) {
            this.x = x;
            this.y = y;
        }
        // 设置为另一点的值
        void init(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 判断一个位置是不是在该点触摸范围内,touchSensitiveRange为触摸有效半径
        boolean isTouchIn(float judgeX, float judgeY) {
            return judgeX < x + touchSensitiveRange &&
                    judgeX > x - touchSensitiveRange &&
                    judgeY < y + touchSensitiveRange &&
                    judgeY > y - touchSensitiveRange;
        }

        // 重写equals和hashCode
        @Override
        public boolean equals(Object o) {
            LockPoint p = (LockPoint) o;
            return p.x == x && p.y == y;
        }

        @Override
        public int hashCode() {
            return 2;
        }

        String out(String tag) {
            return tag + " : x = " + x + " , y = " + y;
        }
    }

初始化

  • 初始化九个点的位置,需要根据控件的大小动态计算,因此在onMeare()之后进行
  • 需求是需要将九个点放在控件中间,来适应控件大小的变化,首先确定第一个点距离左边的距离startSpace,两个点之间的距离 =(控件宽度 - 2 * startSpace)/2
        int size = getMeasuredWidth();
        // 将宽高设置为一样的,正方形
        setMeasuredDimension(size, size);
        // 初始化屏幕中的九个点的位置和下标
        initLockPointArray = new LockPoint[9];
        // startSpace 为距离左边的距离,计算九个点的位置,保证九个点放在控件中间
        if (startSpace == AUTO_START_SPACING) {
            //默认是控件的1/4
            startSpace = size / 4;
        }
        // 计算每两个点之间的间隔
        internalSpace = (size - 2 * startSpace) / 2;
  • 初始化九个点的位置
            // 初始化九个点的位置
            int index = 0;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
  • onMeasure()完整代码
// onMeasure之后初始化数据
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = getMeasuredWidth();
        // 将宽高设置为一样的,正方形
        setMeasuredDimension(size, size);
        // 初始化屏幕中的九个点的位置和下标
        if (initLockPointArray == null) {
            initLockPointArray = new LockPoint[9];
            // startSpace 为距离左边的距离,计算九个点的位置放在控件中间
            if (startSpace == AUTO_START_SPACING) {
                startSpace = size / 4;
            }
            // 计算每两个点之间的间隔
            internalSpace = (size - 2 * startSpace) / 2;
            // 初始化九个点的位置
            int index = 0;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
            // 为了在preview时能看到效果
            if (isInEditMode()) {
                historyPointList.addAll(Arrays.asList(initLockPointArray));
            }
        }
    }

onDraw

  • 绘制过程大致分为三个步骤

  • <1> 绘制九个点,这是每次都需要绘制的

        LockPoint tempPoint;
        for (int i = 0; i < initLockPointArray.length; i++) {
            canvas.drawCircle(initLockPointArray[i].x, initLockPointArray[i].y, pointRadius, pointPaint);
        }
  • <2> 绘制已经划过的点
        // 绘制之前触过存储起来的的点,绘制第i个点和i+1个点之间的线
        if (historyPointList.size() > 0) {
            for (int i = 0; i < historyPointList.size() - 1; i++) {
                canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + 1).x, historyPointList.get(i + 1).y, linePaint);
            }
        }
  • <3> 绘制触摸点和最后一个点的连线
        // 画最后一个点和触摸的点之间的线
        if (currentLockPoint != null
                && currentLockPoint.x != -1 && currentLockPoint.y != -1
                && touchPoint.x != -1 && touchPoint.y != -1) {
            canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);
        }

事件处理

  • 对用户touch事件进行处理
    1. 要记录当前触摸的点,用于绘制跟随手指的连线
    2. 检测触摸的点是不是在九个点中某个点的范围内,如果是的话该点要加入被触摸点的列表中
    3. 当手指抬起时,清除数据,恢复初始状态
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (!isEnabled() || isEventOver)
            return false;

        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化触摸点
            case MotionEvent.ACTION_DOWN:
                touchPoint.init(event.getX(), event.getY());
                break;
            // 移动时检测是否在触摸范围内
            case MotionEvent.ACTION_MOVE:
                touchPoint.init(event.getX(), event.getY());
                LockPoint tempPoint;
                for (int i = 0; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(), event.getY())) {
                        historyPointList.add(new LockPoint(tempPoint));
                        currentLockPoint.init(tempPoint);
                        break;
                    }
                }
                break;
            // 抬起时结束,重新初始化
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                touchPoint.init(-1, -1);
                currentLockPoint.init(-1, -1);
                historyPointList.clear();
                break;
        }
        postInvalidate();
        return true;
    }

事件处理优化-多点触控

  • 用户在触摸屏幕时可能有多个手指在操作,上面的代码在单指时没有问题,兼容多点触控的思路是:
    1. 当用户触发down事件时,我们可以获取到一个pointerId,这个id唯一的标志了这个指头,后面发生的所有事件都使用用这个pointerId来获取,只处理这个指头的事件,避免事件的错乱。
    2. 当我们开始的时候标志的那个手指抬起来了怎么办呢,两个解决方法,第一个就是直接结束整个流程,相当于单指时手指抬起。第二个方法就是转移事件,当一个指头抬起时,从该事件中获取还没抬起的手指,更改标志的pointerId,事件就转移到了另一个手指上,我们关心就是新手指的触摸啦
    3. 关于对于事件进行处理的相关机制可以看Android事件机制,写的都是比较基本的东西,后面慢慢完善,不过理解获取多指的事件9⃣️绰绰有余啦
  • 话不多说,上代码,比较需要注意的地方我都标注在注释中,方便查找
    // 处理触摸事件,支持多点触摸
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // fast stop
        if (!isEnabled() || isEventOver)
            return false;
        // pointerIndex 是事件的在event中的下标
        int pointerIndex;
        // 获取事件掩码
        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化触摸点
            case MotionEvent.ACTION_DOWN:
                // pointerId 记录当前激活的pointerId
                activePointerId = event.getPointerId(0);
                // 根据pointerId查找事件在event中的位置
                pointerIndex = event.findPointerIndex(activePointerId);
                // 根据位置获取到具体的事件的坐标,这里获得的坐标就是我们要记住的那个指头的坐标
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移动时还是根据激活的pointerId获取下标index,来进行后续操作,避免事件错乱
                pointerIndex = event.findPointerIndex(activePointerId);
                // pointerIndex < 0表示手指的事件获取不到了,结束响应事件
                if (pointerIndex < 0) {
                    Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    cancelLockDraw();
                    return false;
                }
                // 根据移动的位置获取坐标,初始化touchPoint的值
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                LockPoint tempPoint;
                // 检索触摸点有没有在九个点中的某一个的触摸范围内
                for (int i = 0; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {
                        LockPoint centerPoint = findCenterPoint(tempPoint);
                        // 优化,查找两个点之间的点,后面会有介绍
                        if (!centerPoint.isEmpty()) {
                            activePoint(centerPoint);
                        }
                        activePoint(tempPoint);
                        break;
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                // 多指操作中 非 最后一个手指抬起时触发ACTION_POINTER_UP,此时要获取还在屏幕上的其他手指转移事件的对象
                onSecondaryPointerUp(event);
                break;
            case MotionEvent.ACTION_UP:
                // 最后的手指抬起触发 ACTION_UP
                pointerIndex = event.findPointerIndex(activePointerId);
                if (pointerIndex < 0) {
                    Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    activePointerId = INVALID_POINTER;
                    return false;
                }
                // 发布绘制的结果,可能是监听回调之类的
                publishResult();
                // 置为-1
                activePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_CANCEL:
                // 类似up
                cancelLockDraw();
                activePointerId = INVALID_POINTER;
                break;
        }
        postInvalidate();
        return true;
    }
  • 转移焦点的方法,在各种控件的源代码中随处可见,我也是拷贝出来直接用的,逻辑不是很复杂
    /**
     * 当一个手机抬起时,转移焦点
     *
     * @param ev 事件
     */
private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }
  • 发布结果
    /**
     * 发布绘制结果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 输入合法
                touchPoint.init(currentLockPoint);
            } else {
                // 输入不合法
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }
  • 回复初始状态,因为在多处调用了,贴一下
    /**
     * 结束绘制,恢复初始状态
     */
    private void cancelLockDraw() {
        touchPoint.init(-1, -1);
        currentLockPoint.init(-1, -1);
        historyPointList.clear();
        postInvalidate();
    }

优化-自动添加两点之间连线上的点

  • 当滑动时越过中间的点之间连接两端,自动查找和添加两点之间的点,手机上的滑动解锁也是这样的逻辑,不然会导致图形很繁琐,不美观而且不符合常见逻辑。也就是说如果当前激发的点和上一个激发的点之间有没有激发的点,那么自动给他激发。

  • 首先如果两个点是相邻的或者是对角线上相邻,那么中间一定不会有空下来的点,需要排除这个情况


    /**
     * 检测相邻
     *
     * @param p1 点1
     * @param p2 点2
     * @return p1和p2是否相邻,斜对角也算相邻
     */
    private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {
        // internalSpace是初始化时两个点之间的距离,都是简单的计算和情况罗列
        if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)
                || (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)
                || (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {
            Log.e(TAG, "相邻点,不处理");
            return true;
        }
        return false;
    }
  • 然后如何判断一个点位于首尾两个激发点的中间,思路是当这个点在两个点的连线上时且不是首尾两个点就是中间的点。判断的根据是斜率是不是相等,就是初中的数学问题啦。
    /**
     * 判断c点是不是在p1-p2的直线上
     *
     * @param p1 起始点
     * @param p2 终止点
     * @param c  判断的点
     * @return 是否在该线上
     */
    private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {
        float k1 = (p1.x - p2.x) * 1f / (p1.y - p2.y);
        float k2 = (p1.x - c.x) * 1f / (p1.y - c.y);
        return k1 == k2;
    }
  • 最后整合一下,去掉不必要的判断,在touch事件中调用
    /**
     * 检测当前激活的点和上一个激活点之间的是否有没有激发的点
     *
     * @param activePoint 当前被激发的点
     * @return 当前激活的点和上一个激活点之间的是否有没有激发的点,没有返回empty的{@link LockPoint#isEmpty()}
     */
    private LockPoint findCenterPoint(LockPoint activePoint) {
        LockPoint rstPoint = new LockPoint();
        // 只有一个点不需要比较
        if (historyPointList.size() < 1) {
            return rstPoint;
        }
        LockPoint tempPoint;
        // 获取上个点
        LockPoint preActivePoint = historyPointList.get(historyPointList.size() - 1);
        // 两个点是不是相邻的,是相邻的是坚决不会中间有点被空出来的
        if (isAdjacentPoint(preActivePoint, activePoint))
            return rstPoint;

        for (int i = 0; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 没有被触摸过 && 不是首点 && 不是尾点
            if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) { 
                // 在连线上
                if (isInLine(preActivePoint, activePoint, tempPoint)) {
                    Log.e(TAG, "点在线上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));
                    rstPoint.init(tempPoint);
                    break;
                }
            }
        }
        return rstPoint;
    }
  • 在onTouchEvent中调用
        LockPoint centerPoint = findCenterPoint(tempPoint);
        // 优化,查找两个点之间的点
        if (!centerPoint.isEmpty()) {
           activePoint(centerPoint);
        }
        activePoint(tempPoint);

优化-给被触摸的点添加动画

  • 当手指触摸到一个点时,添加一个缩放动画来反馈触摸操作

  • 思路时,当触摸到一个点时使用ValueAnimator开启动画,不断改变半径的值,在绘制时达到实现缩放的效果

    /**
     * 开始缩放动画
     */
    private void startScaleAnimation() {

        if (mScaleAnimator == null) {
            mScaleAnimator = ValueAnimator.ofFloat(1f, scaleMax, 1f);
            mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float scale = (float) animation.getAnimatedValue();
                    // 不断改变半径的值
                    scalePointRadius = pointRadius * scale;
                    postInvalidate();
                }
            });
            mScaleAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                        // 动画结束后初始化回标准半径的值
                    scalePointRadius = pointRadius;
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mScaleAnimator.setDuration(scaleAnimDuration);
        }
        if (mScaleAnimator.isRunning())
            mScaleAnimator.end();
        mScaleAnimator.start();
    }
  • 同时在onDraw()方法中对刚刚触摸的点要进行绘制,更改onDraw()方法中绘制九个点的部分,对刚刚触摸的点使用缩放后的半径绘制。
        // 绘制九个点,当动画在执行时被激活的点会被放大
        LockPoint tempPoint;
        for (int i = 0; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 最后触摸的点
            if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {
                canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);
            } else {
                canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);
            }
        }

回调

  • 使用监听将结果回调给使用者,在ACTION_UP时发布结果
        public interface OnLockFinishListener {
        /**
         * 
         * @param lockView 控件
         * @param passWd 密码
         * @param passWsLength 密码长度
         * @return 当返回true时,画面将会定格在绘制结束后的状态,比如当密码输入正确的时候
         * 返回false时,画面会重新初始化回初始状态,比如密码重新二次输入确认或者密码错误的时候
         */
        boolean onFinish(LockView lockView, String passWd, int passWsLength);
    }


    /**
     * 发布绘制结果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 画面定格
                touchPoint.init(currentLockPoint);
            } else {
                // 恢复初始化
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }

综上

  • 还遗留了一个点,就是自动添加中间的点时应该也是有动画效果的,暂时还没做,有空补上吧,希望大家指正。

完整代码

/**
 * Project  : CdLibsTest
 * Package  : com.march.cdlibstest.widget
 * CreateAt : 2016/11/26
 * Describe : 自定义控件实现九宫格滑动解锁
 *
 * @author chendong
 */
public class LockView extends View {

    public static final String TAG = "LOCK_VIEW";
    private static final int INVALID_POINTER = -1;
    private static final int AUTO_START_SPACING = -1;
    private static final int DEFAULT_MIN_POINT_NUM = 4;
    // 激活的触摸点id
    private int activePointerId = INVALID_POINTER;


    // 四边的间隔,默认是控件的1/4
    private int startSpace;
    // 两点间隔
    private int internalSpace;

    // 点的半径
    private int pointRadius;
    // 动画scale的半径
    private float scalePointRadius;
    // 触摸半径,在点的一定范围内触发
    private int touchSensitiveRange;
    // 线宽度
    private int lineWidth;
    // 点颜色
    private int pointColor;
    // 线颜色
    private int lineColor;

    // 缩放的大小
    private float scaleMax;
    // 动画时间
    private int scaleAnimDuration = 150;
    // 本次绘制结束,调用init()方法恢复初始化
    private boolean isEventOver = false;


    class LockPoint {
        // 点的位置 0-8
        int index;
        //  点的x,y坐标
        float x, y;
        // 构造方法,初始化一个点
        LockPoint(int index, float x, float y) {
            this.index = index;
            this.x = x;
            this.y = y;
        }
        // 构造方法,从另一个点初始化
        LockPoint(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 默认构造方法,初始化为一个空的点
        LockPoint() {
            this.x = -1;
            this.y = -1;
            this.index = -1;
        }
        // 判断该点是不是一个空的点
        boolean isEmpty() {
            return this.x == -1 && this.y == -1;
        }
        // 重新给位置赋值
        void init(float x, float y) {
            this.x = x;
            this.y = y;
        }
        // 设置为另一点的值
        void init(LockPoint p) {
            this.x = p.x;
            this.y = p.y;
            this.index = p.index;
        }
        // 判断一个位置是不是在该点触摸范围内,touchSensitiveRange为触摸有效半径
        boolean isTouchIn(float judgeX, float judgeY) {
            return judgeX < x + touchSensitiveRange &&
                    judgeX > x - touchSensitiveRange &&
                    judgeY < y + touchSensitiveRange &&
                    judgeY > y - touchSensitiveRange;
        }

        // 重写equals和hashCode
        @Override
        public boolean equals(Object o) {
            LockPoint p = (LockPoint) o;
            return p.x == x && p.y == y;
        }

        @Override
        public int hashCode() {
            return 2;
        }

        String out(String tag) {
            return tag + " : x = " + x + " , y = " + y;
        }
    }

    // 动画
    private ValueAnimator mScaleAnimator;
    // 初始化的九个点
    private LockPoint[] initLockPointArray;
    // 触摸过的点泪飙
    private List<LockPoint> historyPointList;

    // 触摸的点
    private LockPoint touchPoint;
    // 当前最后一个激活的点
    private LockPoint currentLockPoint;

    // 画线
    private Paint linePaint;
    // 画点
    private Paint pointPaint;

    // 监听
    private OnLockFinishListener listener;

    public interface OnLockFinishListener {
        /**
         *
         * @param lockView 控件
         * @param passWd 密码
         * @param passWsLength 密码长度
         * @return 当返回true时,画面将会定格在绘制结束后的状态
         * 返回false时,画面会重新初始化回初始状态
         */
        boolean onFinish(LockView lockView, String passWd, int passWsLength);
    }

    public LockView(Context context) {
        this(context, null);
    }

    public LockView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
        float density = getResources().getDisplayMetrics().density;

        pointRadius = (int) typedArray.getDimension(R.styleable.LockView_lock_pointRadius, (8 * density));
        scalePointRadius = pointRadius;
        touchSensitiveRange = (int) typedArray.getDimension(R.styleable.LockView_lock_touchSensitiveRange, pointRadius * 3);
        startSpace = (int) typedArray.getDimension(R.styleable.LockView_lock_startSpace, AUTO_START_SPACING);
        lineWidth = (int) typedArray.getDimension(R.styleable.LockView_lock_lineWidth, (5 * density));

        lineColor = typedArray.getColor(R.styleable.LockView_lock_lineColor, Color.WHITE);
        pointColor = typedArray.getColor(R.styleable.LockView_lock_pointColor, Color.WHITE);

        scaleAnimDuration = typedArray.getInt(R.styleable.LockView_lock_scaleAnimDuration, 180);
        scaleMax = typedArray.getFloat(R.styleable.LockView_lock_scaleMax, 2.5f);
        typedArray.recycle();

        historyPointList = new ArrayList<>();
        touchPoint = new LockPoint();
        currentLockPoint = new LockPoint();

        pointPaint = new Paint();
        pointPaint.setAntiAlias(true);
        pointPaint.setColor(pointColor);
        pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(lineWidth);
        linePaint.setColor(lineColor);
        linePaint.setStyle(Paint.Style.STROKE);
    }

    public void setListener(OnLockFinishListener listener) {
        this.listener = listener;
    }


    /**
     * 开始缩放动画
     */
    private void startScaleAnimation() {

        if (mScaleAnimator == null) {
            mScaleAnimator = ValueAnimator.ofFloat(1f, scaleMax, 1f);
            mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float scale = (float) animation.getAnimatedValue();
                    scalePointRadius = pointRadius * scale;
                    postInvalidate();
                }
            });
            mScaleAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    scalePointRadius = pointRadius;
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mScaleAnimator.setDuration(scaleAnimDuration);
        }
        if (mScaleAnimator.isRunning())
            mScaleAnimator.end();
        mScaleAnimator.start();
    }

    // 处理触摸事件,支持多点触摸
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // fast stop
        if (!isEnabled() || isEventOver)
            return false;
        // pointerIndex 是事件的在event中的下标
        int pointerIndex;
        // 获取事件掩码
        int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            // 重新初始化触摸点
            case MotionEvent.ACTION_DOWN:
                // pointerId 记录当前激活的pointerId
                activePointerId = event.getPointerId(0);
                // 根据pointerId查找事件在event中的位置
                pointerIndex = event.findPointerIndex(activePointerId);
                // 根据位置获取到具体的事件的坐标,这里获得的坐标就是我们要记住的那个指头的坐标
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移动时还是根据激活的pointerId获取下标index,来进行后续操作,避免事件错乱
                pointerIndex = event.findPointerIndex(activePointerId);
                // pointerIndex < 0表示手指的事件获取不到了,结束响应事件
                if (pointerIndex < 0) {
                    Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    cancelLockDraw();
                    return false;
                }
                // 根据移动的位置获取坐标,初始化touchPoint的值
                touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));
                LockPoint tempPoint;
                // 检索触摸点有没有在九个点中的某一个的触摸范围内
                for (int i = 0; i < initLockPointArray.length; i++) {
                    tempPoint = initLockPointArray[i];
                    if (!historyPointList.contains(tempPoint)
                            && tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {
                        LockPoint centerPoint = findCenterPoint(tempPoint);
                        if (!centerPoint.isEmpty()) {
                            activePoint(centerPoint);
                        }
                        activePoint(tempPoint);
                        break;
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                // 多指操作中 非 最后一个手指抬起时触发ACTION_POINTER_UP,此时要获取还在屏幕上的其他手指转移事件的对象
                onSecondaryPointerUp(event);
                break;
            case MotionEvent.ACTION_UP:
                // 最后的手指抬起触发 ACTION_UP
                pointerIndex = event.findPointerIndex(activePointerId);
                if (pointerIndex < 0) {
                    Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    activePointerId = INVALID_POINTER;
                    return false;
                }
                // 发布绘制的结果,可能是监听回调之类的
                publishResult();
                // 置为-1
                activePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_CANCEL:
                // 类似up
                cancelLockDraw();
                activePointerId = INVALID_POINTER;
                break;
        }
        postInvalidate();
        return true;
    }


    /**
     * 发布绘制结果
     */
    private void publishResult() {
        if (listener != null) {
            isEventOver = true;
            StringBuilder sb = new StringBuilder();
            for (LockPoint lockPoint : historyPointList) {
                sb.append(lockPoint.index);
            }
            String passWd = sb.toString();
            boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());
            if (isFinish) {
                // 输入合法
                touchPoint.init(currentLockPoint);
            } else {
                // 输入不合法
                cancelLockDraw();
                isEventOver = false;
            }
        } else {
            cancelLockDraw();
        }
    }

    /**
     * 当一个手机抬起时,转移焦点
     *
     * @param ev 事件
     */
    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }

    /**
     * 检测当前激活的点和上一个激活点之间的是否有没有激发的点
     *
     * @param activePoint 当前被激发的点
     * @return 当前激活的点和上一个激活点之间的是否有没有激发的点,没有返回empty的{@link LockPoint#isEmpty()}
     */
    private LockPoint findCenterPoint(LockPoint activePoint) {
        LockPoint rstPoint = new LockPoint();
        // 只有一个点不需要比较
        if (historyPointList.size() < 1) {
            return rstPoint;
        }
        LockPoint tempPoint;
        // 获取上个点
        LockPoint preActivePoint = historyPointList.get(historyPointList.size() - 1);
        // 两个点是不是相邻的,是相邻的是坚决不会中间有点被空出来的
        if (isAdjacentPoint(preActivePoint, activePoint))
            return rstPoint;

        for (int i = 0; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            // 没有被触摸过 && 不是首点 && 不是尾点
            if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) {
                if (isInLine(preActivePoint, activePoint, tempPoint)) {
                    Log.e(TAG, "点在线上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));
                    rstPoint.init(tempPoint);
                    break;
                }
            }
        }
        return rstPoint;
    }


    /**
     * 检测相邻
     *
     * @param p1 点1
     * @param p2 点2
     * @return p1和p2是否相邻,斜对角也算相邻
     */
    private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {
        if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)
                || (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)
                || (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {
            Log.e(TAG, "相邻点,不处理");
            return true;
        }
        return false;
    }


    /**
     * 判断c点是不是在p1-p2的直线上
     *
     * @param p1 起始点
     * @param p2 终止点
     * @param c  判断的点
     * @return 是否在该线上
     */
    private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {
        float k1 = (p1.x - p2.x) * 1f / (p1.y - p2.y);
        float k2 = (p1.x - c.x) * 1f / (p1.y - c.y);
        return k1 == k2;
    }

    /**
     * 激活该点,该点将会添加到选中点列表中,然后执行动画
     *
     * @param tempPoint 被激活的点
     */
    private void activePoint(LockPoint tempPoint) {
        historyPointList.add(new LockPoint(tempPoint));
        currentLockPoint.init(tempPoint);
        startScaleAnimation();
        postInvalidate();
    }


    public void init() {
        isEventOver = false;
        cancelLockDraw();
    }

    /**
     * 结束绘制,恢复初始状态
     */
    private void cancelLockDraw() {
        touchPoint.init(-1, -1);
        currentLockPoint.init(-1, -1);
        historyPointList.clear();
        postInvalidate();
    }


    // onMeasure之后初始化数据
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = getMeasuredWidth();
        // 将宽高设置为一样的,正方形
        setMeasuredDimension(size, size);
        // 初始化屏幕中的九个点的位置和下标
        if (initLockPointArray == null) {
            initLockPointArray = new LockPoint[9];
            // startSpace 为距离左边的距离,计算九个点的位置放在控件中间
            if (startSpace == AUTO_START_SPACING) {
                startSpace = size / 4;
            }
            // 计算每两个点之间的间隔
            internalSpace = (size - 2 * startSpace) / 2;
            // 初始化九个点的位置
            int index = 0;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);
                    index++;
                }
            }
            // 为了在preview时能看到效果
            if (isInEditMode()) {
                historyPointList.addAll(Arrays.asList(initLockPointArray));
            }
        }
    }

    private void log(Object... objs) {
        StringBuilder sb = new StringBuilder();
        for (Object obj : objs) {
            sb.append(obj.toString()).append("   ");
        }
        Log.e(TAG, sb.toString());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // fast stop
        if (initLockPointArray == null)
            return;

        log(currentLockPoint.out("current"), touchPoint.out("touch"));

        // 画最后一个点和触摸的点之间的线
        if (currentLockPoint != null
                && currentLockPoint.x != -1 && currentLockPoint.y != -1
                && touchPoint.x != -1 && touchPoint.y != -1) {
            canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);
        }

        // 绘制之前触过存储起来的的点
        if (historyPointList.size() > 0) {
            for (int i = 0; i < historyPointList.size() - 1; i++) {
                canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + 1).x, historyPointList.get(i + 1).y, linePaint);
            }
        }

        // 绘制九个点,当动画在执行时被激活的点会被放大
        LockPoint tempPoint;
        for (int i = 0; i < initLockPointArray.length; i++) {
            tempPoint = initLockPointArray[i];
            if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {
                canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);
            } else {
                canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);

            }
        }
    }
}
作者:chendong_ 发表于2016/11/28 22:50:49 原文链接
阅读:26 评论:0 查看评论

Android动画学习笔记(三)—基础动画小结

$
0
0

前言:又有一段时间没写博客了,本来打算一个星期搞定Android动画学习的,但是计划跟不上变化哈,最近在赶项目,不多说了,继续我的Android动画学习,加油!骚年~~~~

前面介绍了基础动画的一些知识,感兴趣的童鞋可以去看看我前面两篇博客,文采有点不好,凑合看吧,O(∩_∩)O哈哈~

今天主要是对前面知识的一个我个人的总结,说不定你以前也跟我犯过同样的错误哦!!大牛勿喷哈(^__^)

package com.cisetech.animationdemo.demo.practise;

/**
 * author:yinqingy
 * date:2016-11-23 14:53
 * blog:http://blog.csdn.net/vv_bug
 * desc:基础动画练习
 */

public class BasicPractiseActivity1 extends ListActivity {
    private static final String TITLE = "TITLE";
    private static final String RESID = "RESID";
    private List<Map<String,Object>> datas=new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().getDecorView().setBackgroundColor(Color.WHITE);
        getListView().setBackgroundColor(Color.DKGRAY);
        FrameLayout.LayoutParams lp= (FrameLayout.LayoutParams) getListView().getLayoutParams();
        lp.topMargin=10;
        lp.bottomMargin=10;
        lp.leftMargin=10;
        lp.rightMargin=10;
        initDatas();
        setListAdapter(new SimpleAdapter(this,datas,android.R.layout.simple_list_item_1,
                new String[]{TITLE},new int[]{android.R.id.text1}));
    }
    private void initDatas() {
        addData("alpha(从0.1到1透明度增加)", R.anim.pra_anim_alpha1);
        addData("alpha+rotate", R.anim.pra_anim_alpha_rotate);
        addData("alpha+scale", R.anim.pra_anim_alpha_scale);
        addData("alpha+scale 2", R.anim.pra_anim_alpha_scale2);
        addData("自定义动画1", R.anim.pra_anim_own_design);
        addData("slide left in", R.anim.pra_anim_slide_left);
        addData("slide right in", R.anim.pra_anim_slide_right);
        addData("slide right up", R.anim.pra_anim_slide_up);
        addData("slide right bottom", R.anim.pra_anim_slide_bottom);
        addData("Zoom enter", R.anim.pra_zoom_enter);
        addData("Zoom exit", R.anim.pra_zoom_exit);
        addData("shake test", R.anim.pra_anim_shake);
    }
    private void addData(String title,int resId){
        Map<String,Object>demo=new TreeMap<>();
        demo.put(TITLE,title);
        demo.put(RESID,resId);
        datas.add(demo);
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        ListView listView=getListView();
        Animation a= AnimationUtils.loadAnimation(this, (Integer) datas.get(position).get(RESID));
        if((Integer) datas.get(position).get(RESID)==R.anim.pra_anim_shake){
            Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
            vibrator.cancel();
            long [] pattern = {100,200};   // 停止 开启
            vibrator.vibrate(pattern,-1);
        }
        listView.startAnimation(a);
    }
}

第一个要实现的效果(alpha(从0.1到1透明度增加):
这里写图片描述

显示效果为变化先快后慢的效果,我们加了一个减速插值器。

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.1"
    android:toAlpha="1"
    android:duration="3000"
  android:interpolator="@android:anim/decelerate_interpolator"
    />

代码很简单,旋转、缩放、平移的我就不演示了,小伙伴一定要多练哦,孰能生巧,上一节说了插值器的用法还有解析,那么如果我们自己要定义一个插值器该怎么做能?

我们创建一个文件叫MyInterpolator.java 实现Interpolator接口,然后重写getInterpolation方法:

package com.cisetech.animationdemo;

import android.view.animation.Interpolator;

/**
 * author:yinqingy
 * date:2016-11-28 21:35
 * blog:http://blog.csdn.net/vv_bug
 * desc:
 */

public class MyInterpolator implements Interpolator {
    @Override
    public float getInterpolation(float input) {
        return input;
    }
}

运行代码:
这里写图片描述

可看到,跟我们前面运行时的效果是一样的,因为我们的插值器没有对动画的input做任何处理,直接返回了,所以可看出是匀速运动的。

我们改改代码,对input对一下处理:

public class MyInterpolator implements Interpolator {
    @Override
    public float getInterpolation(float input) {
        return 1-input;
    }
}

我们仅仅是返回了1-input,然后我们再次运行代码:

我就不上图了,运行效果是反的,也就是透明度从1-0.1转变了。
其它复杂的插值器我就不演示了哈,因为我也写不出来,O(∩_∩)O哈哈~!数学物理好的自己好好研究研究哈。

下面重点说一下AnimationSet(联合动画)

AnimationSet就像名字描述的那样,动画的一个集合,就是用一个集合把动画一个一个的装起来,然后同时播放,我们撸一撸它的源码:

首先把动画一个一个装起来:

 public void addAnimation(Animation a) {
        mAnimations.add(a);
        ......
    }

可以看到,第一行代码就是 mAnimations.add(a);我们看看mAnimations是个什么东西?

 private ArrayList<Animation> mAnimations = new ArrayList<Animation>();

看到了没?是不是就是一个集合呢!那么它又是怎么同时播放动画的呢?

首先AnimationSet本身就是一个动画,当我们执行view.startAnimation(set)的时候,在我们的第一节中我有带着一起走了一遍动画的代码,我们可以知道,当开始动画的时候,会调用view的invalidate()方法—->view的onDraw方法会执行—->判断view是否有动画—->动画的getTransformation方法—->对view的canvas的矩阵matrix进行改变(透明度、平移、旋转、缩放)。

我们看看AnimationSet的getTransformation方法:

  @Override
    public boolean getTransformation(long currentTime, Transformation t) {
        final int count = mAnimations.size();
        final ArrayList<Animation> animations = mAnimations;
        final Transformation temp = mTempTransformation;

        boolean more = false;
        boolean started = false;
        boolean ended = true;

        t.clear();

        for (int i = count - 1; i >= 0; --i) {
            final Animation a = animations.get(i);

            temp.clear();
            more = a.getTransformation(currentTime, temp, getScaleFactor()) || more;
            t.compose(temp);

            started = started || a.hasStarted();
            ended = a.hasEnded() && ended;
        }

        if (started && !mStarted) {
            if (mListener != null) {
                mListener.onAnimationStart(this);
            }
            mStarted = true;
        }

        if (ended != mEnded) {
            if (mListener != null) {
                mListener.onAnimationEnd(this);
            }
            mEnded = ended;
        }

        return more;
    }

我们看到,有一个for循环,然后同时播放集合中的所有动画。
既然里面放的是集合,那么有一些方法一定是作用于所有的Animation的:
我们看到了一个执行动画前初始化动画的方法:

   /**
     * @see android.view.animation.Animation#initialize(int, int, int, int)
     */
    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        }

那么initialize又是谁去初始化的呢? 想必小伙伴猜都猜到了,肯定是view调用的,于是我们到view中去搜索一下,

在view中我们找到了这么一段代码:

 final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

看到了这,小伙伴是不是有点明白了!!

那么我们的AnimationSet初始化的时候都干了什么呢?

   boolean durationSet = (mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK;
        boolean fillAfterSet = (mFlags & PROPERTY_FILL_AFTER_MASK) == PROPERTY_FILL_AFTER_MASK;
        boolean fillBeforeSet = (mFlags & PROPERTY_FILL_BEFORE_MASK) == PROPERTY_FILL_BEFORE_MASK;
        boolean repeatModeSet = (mFlags & PROPERTY_REPEAT_MODE_MASK) == PROPERTY_REPEAT_MODE_MASK;
        boolean shareInterpolator = (mFlags & PROPERTY_SHARE_INTERPOLATOR_MASK)
                == PROPERTY_SHARE_INTERPOLATOR_MASK;
        boolean startOffsetSet = (mFlags & PROPERTY_START_OFFSET_MASK)
                == PROPERTY_START_OFFSET_MASK;

首先判断我们的AnimationSet中有没有设置这些属性,如:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:startOffset="3000"
    >

我们在set标签中给了两个属性android:duration跟android:startOffset,然后AnimationSet拿到这两个属性后,就会走initialize方法,然后就会把属性值赋值给所有的Animation:

 for (int i = 0; i < count; i++) {
            Animation a = children.get(i);
            if (durationSet) {
                a.setDuration(duration);
            }
            if (fillAfterSet) {
                a.setFillAfter(fillAfter);
            }
            if (fillBeforeSet) {
                a.setFillBefore(fillBefore);
            }
            if (repeatModeSet) {
                a.setRepeatMode(repeatMode);
            }
            if (shareInterpolator) {
                a.setInterpolator(interpolator);
            }
            if (startOffsetSet) {
                long offset = a.getStartOffset();
                a.setStartOffset(offset + startOffset);
                storedOffsets[i] = offset;
            }
            a.initialize(width, height, parentWidth, parentHeight);
        }

这里写图片描述

效果很简单,我直接上代码了:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    >
    <alpha
        android:fromAlpha="0.5"
        android:toAlpha="1"
        />
    <rotate
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />
</set>

然后我们想让alpha执行完毕后再去执行rotate,我们改改代码:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    >
    <alpha
        android:fromAlpha="0.5"
        android:toAlpha="1"
        />
    <rotate
        android:startOffset="3000"
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />
</set>

让rotate在3s后再运行:
这里写图片描述

效果是达到了,然后我们又想让我们的透明度从0-1然后再执行1-0,然后再执行rotate,我们改改代码:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    >
    <alpha
        android:fromAlpha="0.5"
        android:toAlpha="1"
        android:repeatCount="1"
        android:repeatMode="reverse"
        />
    <rotate
        android:startOffset="3000"
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />
</set>

我们再次运行代码:
这里写图片描述

我们发现,透明度从0-1是对的,然后我们的透明度1-0是跟rotate动画一起执行的。

我们发现问题?repeatCount设置成了几的话此动画就会连续播放几次,也就是说我们的alpha执行完0.5-1的时候已经花了3s了,然后当再执行1-0.5的时候跟rotate一起执行了,所以按照我们现在的说法的话,我们要达到我们一开始说的那种效果的话(执行完0-1然后执行完1-0 最后执行rotate动画)我们需要把rotate的延时改为6s:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    >
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:repeatCount="1"
        android:repeatMode="reverse"
        />
    <rotate
        android:startOffset="6000"
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />
</set>

我们再次运行代码:
这里写图片描述

我们可以看到,执行了0-1然后1-0,但是rotate不执行了。。。 尼玛!!!这是什么情况? 因为当我们执行rotate的时候,此时我们view的canvas的状态为透明度0,也就是说隐藏了,,,所以此时执行rotate的话,默认是看不到的!!!

为了验证我们的推测,我们改改最后执行的透明度值,把0-1 1-0改为0.2-1 1-0.2

再次执行代码:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    >
    <alpha
        android:fromAlpha="0.2"
        android:toAlpha="1"
        android:repeatCount="1"
        android:repeatMode="reverse"
        />
    <rotate
        android:startOffset="6000"
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />
</set>

这里写图片描述

怎么样?是不是跟我们预期效果是一样的呢??

最后调查一下:有朋友把repeatCount=1看做成“一来一回”然后repeatCount=2看做成“一来一回,一来一回,往返两次”的请举手!!!O(∩_∩)O哈哈~ 我最初就是这么以为的,直到写博客前还这么认为,所以有些东西还是得多敲多练哈,不能只凭字面上掌握了就说自己掌握了额

demo还有一些我就不演示了,我已经提交到github了,包括前面两篇文章的demo

https://github.com/913453448/AnDemo

作者:vv_bug 发表于2016/11/28 22:53:27 原文链接
阅读:18 评论:0 查看评论

Android Multimedia框架总结(二十一)MediaCodec中创建到start过程(到jni部分)

$
0
0

转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53386117

上一章介绍MediaCodec的说明及状态图,从今天开始,将深入源码中看看其过程,看下Agenda如下:

  • 一张图看清MediaCodec从创建到start过程
  • 补充MediaCodec基本用法
  • MediaCodec中BufferInfo内部类:
  • android_media_MediaCodec.cpp
  • android_media_MediaCodec.h

一张图看清MediaCodec从创建到start过程(到jni部分)

这里写图片描述

补充MediaCodec基本用法

MediaCodec的使用遵循一个基本模式:

  • 1.创建和配置MediaCodec对象
  • 2.进行以下循环:

    • 如果一个输入缓冲区准备好:
    • 读取部分数据,复制到缓冲区
    • 如果一个输出缓冲区准备好:
    • 复制到缓冲区
  • 3.销毁MediaCodec对象

一个MediaCodec对象可以对特定类型的数据(MP3音频或H.264视频)进行编码或解码。因为是在原始数据上操作,所以任何文件头(比如ID3 tags)必须被剔除,MediaCodec不与任何更高层次的内容交互,所以无法通过扬声器播放音频或者从网络接收视频流。它只将缓冲区数据读入,再输出到缓冲区。MediaCodec可以把大部分的外层数据去掉。

有些编解码器对Buffer非常挑剔。比如,Buffer必须满足特定的内存对其方式,或者某个最值尺寸,或者同时满足几点。为了更强的兼容性,编解码器从应用程序获取分配Buffer的权限。所以,不是装有数据的Buffer直接给MediaCodec,而是想MediaCodec请求一个Buffer,再把数据拷进去。

这样好像和“zero-copy”原则相违背,但其实在大多数情况下并不需要拷贝,因为编译码器并不是非得拷贝或调整数据来满足要求(What?)。在某些情况下,你可以直接使用那个Buffer,比如直接从硬盘或网络读取数据到Buffer,所以拷贝不是必须的(这不就是拷贝吗?)

MediaCodec的输入必须处理成特定的格式。H264视频编码的时候输入就是一帧数据,H264解码指的就是一个NAL单元。但你不可能一次只提交单个数据或数据在需要处理的时候才出现(待商榷),这样看起来,输入更像是一个流。实际上,编解码器在输出前同时拥有多个Buffer。

MediaCodec中BufferInfo内部类:
这里写图片描述

本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/53386117

MediaCodec和MediaPlayer在很多地方有相似之处,当java层调用MediaCodec.createByCodecName,MediaCodec.createDecoderByType,MediaCodec.createEncoderByType都会到达MediaCodec的构造,构造中都会调用native_setup,如下:

这里写图片描述

其中对应到有这么一段,相当于是作了一次映射

这里写图片描述

接着进入android_media_MediaCodec_native_setup函数

这里写图片描述

setMediaCodec函数如下:

这里写图片描述

接下来看下JMediaCodec的构造

这里写图片描述

以上几个步骤到得到MediaCodec对像后,就到达Java层调用MediaCodec.configure(format,surface,null,0)

这里写图片描述

通过获取format中map,就是一个hashmap,便利视频源的格式放到两个数组中,然后,再通过native_configure向下传递

这里写图片描述
这里写图片描述

本文出自逆流的鱼yuiop:
http://blog.csdn.net/hejjunlin/article/details/53386117

这里写图片描述

当调用start后,public native final void start();

这里写图片描述

JMediaCodec中start

这里写图片描述

最后看下对应jni的android_media_MediaCodec.h如下:

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

第一时间获得博客更新提醒,以及更多android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。


这里写图片描述

如果你觉得好,随手点赞,也是对笔者的肯定,也可以分享此公众号给你更多的人,原创不易

作者:hejjunlin 发表于2016/11/28 23:03:37 原文链接
阅读:37 评论:0 查看评论

SnapHelper

$
0
0

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/53386286


SnapHelperAndroid Support Library reversion 24.2.0 新增加的API。


SnapHelper 的应用

SnapHelper 是RecyclerView的一个辅助工具类。

它实现了RecyclerView.onFlingListener接口。而RecyclerView.onFlingListener 是一个用来响应用户手势滑动的接口。

SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper子类,可以实现类似ViewPager的滚动效果,滑动结束之后让某个item停留在中间位置。

这里写图片描述

效果类似于Google Play主界面中item的滚动效果。




LinearSnapHelper的使用很简单,只需要调用 attachToRecyclerView(xxx) ,绑定上一个RecyclerView即可。

上一张自己的效果图:

这里写图片描述


LinearSnapHelper 源码分析

下面来分析一下 LinearSnapHelper

先从 attachToRecyclerView() 入手。

public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            setupCallbacks();
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            snapToTargetExistingView();
        }
    }

destoryCallback() 作用在于取消之前的RecyclerView的监听接口。

/**
 * Called when the instance of a {@link RecyclerView} is detached.
 */
    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }

setupCallbacks() – 设置监听器

/**
     * Called when an instance of a {@link RecyclerView} is attached.
     */
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

此时可以看到,如果当前RecyclerView已经设置了OnFlingListener,会抛出一个 状态异常


snapToTargetExistingView()

/**
 * 找到居中显示的view,计算它的位置,调用smoothScrollBy使其居中
 */
void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        // 计算目标View需要移动的距离
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

该方法中显示调用 findSnapView() 找到目标View(需要居中显示的View),然后调用 calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离。这两个方法均需要LinearSnapHelper重写。


SnapHelper.java 中有三个抽象函数需要LinearSnapHelper 重写。

/**
 * 找到那个“snapView”
 */
public abstract View findSnapView(LayoutManager layoutManager);
/**
 * 计算targetView需要移动的距离
 * 该方法返回一个二维数组,分别表示X轴、Y轴方向上需要修正的偏移量
 */
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
            @NonNull View targetView);
/**
 * 根据速度找到将要滑到的position
 */
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
            int velocityY);

在setupCallbacks() 方法中可以看到对RecyclerView 设置了 OnScrollListener OnFlingListener 两个监听器。

查看SnapHelper可以发现:

// Handles the snap on scroll case.
    private final RecyclerView.OnScrollListener mScrollListener =
            new RecyclerView.OnScrollListener() {
                boolean mScrolled = false;

                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                        mScrolled = false;
                        snapToTargetExistingView();
                    }
                }

                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (dx != 0 || dy != 0) {
                        mScrolled = true;
                    }
                }
            };

    @Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

当滚动结束是,会调用 snapToTargetExistingView() 方法。

而当手指滑动触发onFling() 函数时,会根据X轴、Y轴方向上的速率加上 snapFromFling() 方法的返回值综合判断。


看一下 snapFromFling()

/**
     * Helper method to facilitate for snapping triggered by a fling.
     *
     * @param layoutManager The {@link LayoutManager} associated with the attached
     *                      {@link RecyclerView}.
     * @param velocityX     Fling velocity on the horizontal axis.
     * @param velocityY     Fling velocity on the vertical axis.
     *
     * @return true if it is handled, false otherwise.
     */
    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }

        // 创建SmoothScroll对象
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }

        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }

        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

接下来看LinearSnapHelper.java 复写的三个方法

@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return RecyclerView.NO_POSITION;
        }

        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        // 重点在findSnapView()

        final View currentView = findSnapView(layoutManager);
        if (currentView == null) {
            return RecyclerView.NO_POSITION;
        }

        final int currentPosition = layoutManager.getPosition(currentView);
        if (currentPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }

        // ...省略若干代码

        return targetPos;
    }

省略的若干代码主要是根据手势滑动的速率计算目标item的位置。具体算法不用多研究。

可以看到方法内部又调用了 findSnapView() ;

@Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

这里根据LayoutManager的方向做个判断,进而调用 findCenterView() 方法。

/**
     * 返回距离父容器中间位置最近的子View
     */
    @Nullable
    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        final int center; // 中间位值
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

        for (int i = 0; i < childCount; i++) {  // 循环判断子View中间位值距离父容器中间位值的差值
            final View child = layoutManager.getChildAt(i);
            int childCenter = helper.getDecoratedStart(child) +
                    (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - center);

            /** if child center is closer than previous closest, set it as closest  **/
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild; // 返回距离父容器中间位置最近的子View
    }

然后来看 calculateDistanceToFinalSnap()

@Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

定义一个二维数组,根据LayoutManager的方向来判断进行赋值。

private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView) +
                (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        return childCenter - containerCenter;
    }

该方法的目的即是 计算目标View距离父容器中间位值的差值


至此,流程已经分析完毕。

总结如下:

  1. 有速率的滑动,会触发onScrollStateChanged() onFling() 两个方法。

    • onScrollStateChanged() 方法内部调用 findSnapView() 找到对应的View,然后据此View在调用calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离,最后通过RecyclerView.smoothScrollBy() 来移动View。

    • onFling() 方法内部调用 snapFromFling(), 然后在此方法内部首先创建了一个SmoothScroller 对象。接着调用 findTargetSnapPosition() 找到目标View的position,然后对smoothScroller设置该position,最后通过LayoutManager.startSmoothScroll() 开始移动View。

  2. 没有速率的滚动只会触发 onScrollStateChanged() 函数。


扩展

LinearSnapHelper 类的目的是将某个View停留在正中间,我们也可以通过这种方式来实现每次滑动结束之后将某个View停留在最左边或者最右边。

其实通过上面的分析,就会发现最主要的就是 calculateDistanceToFinalSnap findSnapView 这两个函数。

在寻找目标View的时候,不像findCenterView那么简单。
以为需要考虑到最后item的边界情况。判断的不好就会出现,无论怎么滑动都会出现最后一个item无法完整显示的bug。

且看我的代码:

/**
     * 注意判断最后一个item时,应通过判断距离右侧的位置
     *
     * @param layoutManager
     * @param helper
     * @return
     */
    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
            return null;
        }
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        final int start = helper.getStartAfterPadding();

        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedStart(child);
            int absDistance = Math.abs(childStart - start);

            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        // 边界情况判断
        View firstVisibleChild = layoutManager.getChildAt(0);

        if (firstVisibleChild != closestChild) {
            return closestChild;
        }

        int firstChildStart = helper.getDecoratedStart(firstVisibleChild);

        int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
        View lastChild = layoutManager.getChildAt(childCount - 1);
        int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2);
        boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1;
        if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) {
            return lastChild;
        }

        return closestChild;
    }

对于“反向的”同样要考虑边界情况。

private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
            return null;
        }
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) {
            return null;
        }

        View closestChild = null;
        final int end = helper.getEndAfterPadding();

        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedEnd(child);
            int absDistance = Math.abs(childStart - end);

            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        // 边界情况判断
        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);

        if (lastVisibleChild != closestChild) {
            return closestChild;
        }

        if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) {
            return closestChild;
        }

        View firstChild = layoutManager.getChildAt(0);
        int firstChildStart = helper.getDecoratedStart(firstChild);

        int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        boolean isFirstItem = firstChildPos == 0;


        int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2);
        if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) {
            return firstChild;
        }

        return closestChild;
    }

效果图如下:

这里写图片描述

这里写图片描述


完整代码,请移步:

JackSnapHelper.java


完毕,谢谢支持~~

作者:crazy1235 发表于2016/11/28 23:25:34 原文链接
阅读:42 评论:0 查看评论

加载网络图片所显示的转圈效果及加载成功前与失败后所显示的图标

$
0
0


MainActivity的一个调用方法:

    //网络图片下载
    private void downLoadFile(final String url, final String target, final String fileName) {

        //设置成临时文件后缀名
        final String fileTempName = FileUtils.getFileTempName(target);
        HttpUtils http = XUtilsHttpClient.getInstanceManageToFile(MainActivity.this);
        http.download(url, fileTempName,
                true, // 如果目标文件存在,接着未完成的部分继续下载。服务器不支持RANGE时将从新下载。
                true, // 如果从请求返回信息中获取到文件名,下载完成后自动重命名。
                new RequestCallBack<File>() {
                    @Override
                    public void onStart() {

                        progressWheel.spin();//开始时调用的是加载圆圈方法显示
                        progress_wheel_layout.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_image_loading_default));//这里是调用开始下载时的图片
                    }

                    @Override
                    public void onLoading(long total, long current, boolean isUploading) {

                    }

                    @Override
                    public void onSuccess(ResponseInfo<File> responseInfo) {

                        progressWheel.stopSpinning();

                        String filePathName = FileUtils.reductionFileName(fileTempName, fileName);
                        System.out.println(".............filePathName = " + filePathName);
                        showImage(filePathName);


                    @Override
                    public void onFailure(HttpException error, String msg) {
                        Log.e("................", "onFailure: ", error);
                        progressWheel.stopSpinning();
                        Toast.makeText(MainActivity.this.this, "动态图加载失败...", Toast.LENGTH_SHORT).show();
                        progress_wheel_layout.setBackgroundDrawable(getResources().getDrawable(R.drawable.ic_image_loading_failed));//这里是调用下载失败后的图片
                    }
                });
    }

activity_main:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
	
    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <RelativeLayout
                android:id="@+id/progress_wheel_layout"
                android:layout_centerVertical="true"
                android:layout_centerHorizontal="true"
                android:background="@color/black"
                android:layout_width="250dp"
                android:layout_height="250dp">
            <com.pnikosis.materialishprogress.ProgressWheel
                    android:id="@+id/progress_wheel"
                    android:layout_width="80dp"
                    android:layout_height="80dp"
                    android:layout_centerHorizontal="true"
                    android:layout_centerVertical="true"
                    wheel:matProg_barColor="#5588FF"
                    wheel:matProg_progressIndeterminate="true"/>
        </RelativeLayout>
        <!--所要显示的网络图片-->
        <ImageView
                android:id="@+id/image_view"
                android:layout_centerInParent="true"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                />
    </RelativeLayout>
</LinearLayout>

这是自定义的一个类:

package com.pnikosis.materialishprogress;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;

/**
 * A Material style progress wheel, compatible up to 2.2.
 * Todd Davies' Progress Wheel https://github.com/Todd-Davies/ProgressWheel
 *
 * @author Nico Hormazábal
 *         <p/>
 *         Licensed under the Apache License 2.0 license see:
 *         http://www.apache.org/licenses/LICENSE-2.0
 */
public class ProgressWheel extends View {
  private static final String TAG = ProgressWheel.class.getSimpleName();
  private final int barLength = 16;
  private final int barMaxLength = 270;
  private final long pauseGrowingTime = 200;
  /**
   * *********
   * DEFAULTS *
   * **********
   */
  //Sizes (with defaults in DP)
  private int circleRadius = 28;
  private int barWidth = 4;
  private int rimWidth = 4;
  private boolean fillRadius = false;
  private double timeStartGrowing = 0;
  private double barSpinCycleTime = 460;
  private float barExtraLength = 0;
  private boolean barGrowingFromFront = true;
  private long pausedTimeWithoutGrowing = 0;
  //Colors (with defaults)
  private int barColor = 0xAA000000;
  private int rimColor = 0x00FFFFFF;

  //Paints
  private Paint barPaint = new Paint();
  private Paint rimPaint = new Paint();

  //Rectangles
  private RectF circleBounds = new RectF();

  //Animation
  //The amount of degrees per second
  private float spinSpeed = 230.0f;
  //private float spinSpeed = 120.0f;
  // The last time the spinner was animated
  private long lastTimeAnimated = 0;

  private boolean linearProgress;

  private float mProgress = 0.0f;
  private float mTargetProgress = 0.0f;
  private boolean isSpinning = false;

  private ProgressCallback callback;

  private boolean shouldAnimate;

  /**
   * The constructor for the ProgressWheel
   */
  public ProgressWheel(Context context, AttributeSet attrs) {
    super(context, attrs);

    parseAttributes(context.obtainStyledAttributes(attrs, R.styleable.ProgressWheel));

    setAnimationEnabled();
  }

  /**
   * The constructor for the ProgressWheel
   */
  public ProgressWheel(Context context) {
    super(context);
    setAnimationEnabled();
  }

  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void setAnimationEnabled() {
    int currentApiVersion = android.os.Build.VERSION.SDK_INT;

    float animationValue;
    if (currentApiVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      animationValue = Settings.Global.getFloat(getContext().getContentResolver(),
          Settings.Global.ANIMATOR_DURATION_SCALE, 1);
    } else {
      animationValue = Settings.System.getFloat(getContext().getContentResolver(),
          Settings.System.ANIMATOR_DURATION_SCALE, 1);
    }

    shouldAnimate = animationValue != 0;
  }

  //----------------------------------
  //Setting up stuff
  //----------------------------------

  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int viewWidth = circleRadius + this.getPaddingLeft() + this.getPaddingRight();
    int viewHeight = circleRadius + this.getPaddingTop() + this.getPaddingBottom();

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
      //Must be this size
      width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
      //Can't be bigger than...
      width = Math.min(viewWidth, widthSize);
    } else {
      //Be whatever you want
      width = viewWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.EXACTLY) {
      //Must be this size
      height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
      //Can't be bigger than...
      height = Math.min(viewHeight, heightSize);
    } else {
      //Be whatever you want
      height = viewHeight;
    }

    setMeasuredDimension(width, height);
  }

  /**
   * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view,
   * because this method is called after measuring the dimensions of MATCH_PARENT & WRAP_CONTENT.
   * Use this dimensions to setup the bounds and paints.
   */
  @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    setupBounds(w, h);
    setupPaints();
    invalidate();
  }

  /**
   * Set the properties of the paints we're using to
   * draw the progress wheel
   */
  private void setupPaints() {
    barPaint.setColor(barColor);
    barPaint.setAntiAlias(true);
    barPaint.setStyle(Style.STROKE);
    barPaint.setStrokeWidth(barWidth);

    rimPaint.setColor(rimColor);
    rimPaint.setAntiAlias(true);
    rimPaint.setStyle(Style.STROKE);
    rimPaint.setStrokeWidth(rimWidth);
  }

  /**
   * Set the bounds of the component
   */
  private void setupBounds(int layout_width, int layout_height) {
    int paddingTop = getPaddingTop();
    int paddingBottom = getPaddingBottom();
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();

    if (!fillRadius) {
      // Width should equal to Height, find the min value to setup the circle
      int minValue = Math.min(layout_width - paddingLeft - paddingRight,
          layout_height - paddingBottom - paddingTop);

      int circleDiameter = Math.min(minValue, circleRadius * 2 - barWidth * 2);

      // Calc the Offset if needed for centering the wheel in the available space
      int xOffset = (layout_width - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft;
      int yOffset = (layout_height - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop;

      circleBounds =
          new RectF(xOffset + barWidth, yOffset + barWidth, xOffset + circleDiameter - barWidth,
              yOffset + circleDiameter - barWidth);
    } else {
      circleBounds = new RectF(paddingLeft + barWidth, paddingTop + barWidth,
          layout_width - paddingRight - barWidth, layout_height - paddingBottom - barWidth);
    }
  }

  /**
   * Parse the attributes passed to the view from the XML
   *
   * @param a the attributes to parse
   */
  private void parseAttributes(TypedArray a) {
    // We transform the default values from DIP to pixels
    DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
    barWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, barWidth, metrics);
    rimWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rimWidth, metrics);
    circleRadius =
        (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleRadius, metrics);

    circleRadius =
        (int) a.getDimension(R.styleable.ProgressWheel_matProg_circleRadius, circleRadius);

    fillRadius = a.getBoolean(R.styleable.ProgressWheel_matProg_fillRadius, false);

    barWidth = (int) a.getDimension(R.styleable.ProgressWheel_matProg_barWidth, barWidth);

    rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_matProg_rimWidth, rimWidth);

    float baseSpinSpeed =
        a.getFloat(R.styleable.ProgressWheel_matProg_spinSpeed, spinSpeed / 360.0f);
    spinSpeed = baseSpinSpeed * 360;

    barSpinCycleTime =
        a.getInt(R.styleable.ProgressWheel_matProg_barSpinCycleTime, (int) barSpinCycleTime);

    barColor = a.getColor(R.styleable.ProgressWheel_matProg_barColor, barColor);

    rimColor = a.getColor(R.styleable.ProgressWheel_matProg_rimColor, rimColor);

    linearProgress = a.getBoolean(R.styleable.ProgressWheel_matProg_linearProgress, false);

    if (a.getBoolean(R.styleable.ProgressWheel_matProg_progressIndeterminate, false)) {
      spin();
    }

    // Recycle
    a.recycle();
  }

  public void setCallback(ProgressCallback progressCallback) {
    callback = progressCallback;

    if (!isSpinning) {
      runCallback();
    }
  }

  //----------------------------------
  //Animation stuff
  //----------------------------------

  @TargetApi(Build.VERSION_CODES.CUPCAKE)
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawArc(circleBounds, 360, 360, false, rimPaint);

    boolean mustInvalidate = false;

    if (!shouldAnimate) {
      return;
    }

    if (isSpinning) {
      //Draw the spinning bar
      mustInvalidate = true;

      long deltaTime = (SystemClock.uptimeMillis() - lastTimeAnimated);
      float deltaNormalized = deltaTime * spinSpeed / 1000.0f;

      updateBarLength(deltaTime);

      mProgress += deltaNormalized;
      if (mProgress > 360) {
        mProgress -= 360f;

        // A full turn has been completed
        // we run the callback with -1 in case we want to
        // do something, like changing the color
        runCallback(-1.0f);
      }
      lastTimeAnimated = SystemClock.uptimeMillis();

      float from = mProgress - 90;
      float length = barLength + barExtraLength;

      if (isInEditMode()) {
        from = 0;
        length = 135;
      }

      canvas.drawArc(circleBounds, from, length, false, barPaint);
    } else {
      float oldProgress = mProgress;

      if (mProgress != mTargetProgress) {
        //We smoothly increase the progress bar
        mustInvalidate = true;

        float deltaTime = (float) (SystemClock.uptimeMillis() - lastTimeAnimated) / 1000;
        float deltaNormalized = deltaTime * spinSpeed;

        mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress);
        lastTimeAnimated = SystemClock.uptimeMillis();
      }

      if (oldProgress != mProgress) {
        runCallback();
      }

      float offset = 0.0f;
      float progress = mProgress;
      if (!linearProgress) {
        float factor = 2.0f;
        offset = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, 2.0f * factor)) * 360.0f;
        progress = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, factor)) * 360.0f;
      }

      if (isInEditMode()) {
        progress = 360;
      }

      canvas.drawArc(circleBounds, offset - 90, progress, false, barPaint);
    }

    if (mustInvalidate) {
      invalidate();
    }
  }

  @Override protected void onVisibilityChanged(View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);

    if (visibility == VISIBLE) {
      lastTimeAnimated = SystemClock.uptimeMillis();
    }
  }

  private void updateBarLength(long deltaTimeInMilliSeconds) {
    if (pausedTimeWithoutGrowing >= pauseGrowingTime) {
      timeStartGrowing += deltaTimeInMilliSeconds;

      if (timeStartGrowing > barSpinCycleTime) {
        // We completed a size change cycle
        // (growing or shrinking)
        timeStartGrowing -= barSpinCycleTime;
        //if(barGrowingFromFront) {
        pausedTimeWithoutGrowing = 0;
        //}
        barGrowingFromFront = !barGrowingFromFront;
      }

      float distance =
          (float) Math.cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI) / 2 + 0.5f;
      float destLength = (barMaxLength - barLength);

      if (barGrowingFromFront) {
        barExtraLength = distance * destLength;
      } else {
        float newLength = destLength * (1 - distance);
        mProgress += (barExtraLength - newLength);
        barExtraLength = newLength;
      }
    } else {
      pausedTimeWithoutGrowing += deltaTimeInMilliSeconds;
    }
  }

  /**
   * Check if the wheel is currently spinning
   */

  public boolean isSpinning() {
    return isSpinning;
  }

  /**
   * Reset the count (in increment mode)
   */
  public void resetCount() {
    mProgress = 0.0f;
    mTargetProgress = 0.0f;
    invalidate();
  }

  /**
   * Turn off spin mode
   */
  public void stopSpinning() {
    isSpinning = false;
    mProgress = 0.0f;
    mTargetProgress = 0.0f;
    invalidate();
  }

  /**
   * Puts the view on spin mode
   */
  public void spin() {
    lastTimeAnimated = SystemClock.uptimeMillis();
    isSpinning = true;
    invalidate();
  }

  private void runCallback(float value) {
    if (callback != null) {
      callback.onProgressUpdate(value);
    }
  }

  private void runCallback() {
    if (callback != null) {
      float normalizedProgress = (float) Math.round(mProgress * 100 / 360.0f) / 100;
      callback.onProgressUpdate(normalizedProgress);
    }
  }

  /**
   * Set the progress to a specific value,
   * the bar will be set instantly to that value
   *
   * @param progress the progress between 0 and 1
   */
  public void setInstantProgress(float progress) {
    if (isSpinning) {
      mProgress = 0.0f;
      isSpinning = false;
    }

    if (progress > 1.0f) {
      progress -= 1.0f;
    } else if (progress < 0) {
      progress = 0;
    }

    if (progress == mTargetProgress) {
      return;
    }

    mTargetProgress = Math.min(progress * 360.0f, 360.0f);
    mProgress = mTargetProgress;
    lastTimeAnimated = SystemClock.uptimeMillis();
    invalidate();
  }

  // Great way to save a view's state http://stackoverflow.com/a/7089687/1991053
  @Override public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();

    WheelSavedState ss = new WheelSavedState(superState);

    // We save everything that can be changed at runtime
    ss.mProgress = this.mProgress;
    ss.mTargetProgress = this.mTargetProgress;
    ss.isSpinning = this.isSpinning;
    ss.spinSpeed = this.spinSpeed;
    ss.barWidth = this.barWidth;
    ss.barColor = this.barColor;
    ss.rimWidth = this.rimWidth;
    ss.rimColor = this.rimColor;
    ss.circleRadius = this.circleRadius;
    ss.linearProgress = this.linearProgress;
    ss.fillRadius = this.fillRadius;

    return ss;
  }

  @Override public void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof WheelSavedState)) {
      super.onRestoreInstanceState(state);
      return;
    }

    WheelSavedState ss = (WheelSavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());

    this.mProgress = ss.mProgress;
    this.mTargetProgress = ss.mTargetProgress;
    this.isSpinning = ss.isSpinning;
    this.spinSpeed = ss.spinSpeed;
    this.barWidth = ss.barWidth;
    this.barColor = ss.barColor;
    this.rimWidth = ss.rimWidth;
    this.rimColor = ss.rimColor;
    this.circleRadius = ss.circleRadius;
    this.linearProgress = ss.linearProgress;
    this.fillRadius = ss.fillRadius;

    this.lastTimeAnimated = SystemClock.uptimeMillis();
  }

  /**
   * @return the current progress between 0.0 and 1.0,
   * if the wheel is indeterminate, then the result is -1
   */
  public float getProgress() {
    return isSpinning ? -1 : mProgress / 360.0f;
  }

  //----------------------------------
  //Getters + setters
  //----------------------------------

  /**
   * Set the progress to a specific value,
   * the bar will smoothly animate until that value
   *
   * @param progress the progress between 0 and 1
   */
  public void setProgress(float progress) {
    if (isSpinning) {
      mProgress = 0.0f;
      isSpinning = false;

      runCallback();
    }

    if (progress > 1.0f) {
      progress -= 1.0f;
    } else if (progress < 0) {
      progress = 0;
    }

    if (progress == mTargetProgress) {
      return;
    }

    // If we are currently in the right position
    // we set again the last time animated so the
    // animation starts smooth from here
    if (mProgress == mTargetProgress) {
      lastTimeAnimated = SystemClock.uptimeMillis();
    }

    mTargetProgress = Math.min(progress * 360.0f, 360.0f);

    invalidate();
  }

  /**
   * Sets the determinate progress mode
   *
   * @param isLinear if the progress should increase linearly
   */
  public void setLinearProgress(boolean isLinear) {
    linearProgress = isLinear;
    if (!isSpinning) {
      invalidate();
    }
  }

  /**
   * @return the radius of the wheel in pixels
   */
  public int getCircleRadius() {
    return circleRadius;
  }

  /**
   * Sets the radius of the wheel
   *
   * @param circleRadius the expected radius, in pixels
   */
  public void setCircleRadius(int circleRadius) {
    this.circleRadius = circleRadius;
    if (!isSpinning) {
      invalidate();
    }
  }

  /**
   * @return the width of the spinning bar
   */
  public int getBarWidth() {
    return barWidth;
  }

  /**
   * Sets the width of the spinning bar
   *
   * @param barWidth the spinning bar width in pixels
   */
  public void setBarWidth(int barWidth) {
    this.barWidth = barWidth;
    if (!isSpinning) {
      invalidate();
    }
  }

  /**
   * @return the color of the spinning bar
   */
  public int getBarColor() {
    return barColor;
  }

  /**
   * Sets the color of the spinning bar
   *
   * @param barColor The spinning bar color
   */
  public void setBarColor(int barColor) {
    this.barColor = barColor;
    setupPaints();
    if (!isSpinning) {
      invalidate();
    }
  }

  /**
   * @return the color of the wheel's contour
   */
  public int getRimColor() {
    return rimColor;
  }

  /**
   * Sets the color of the wheel's contour
   *
   * @param rimColor the color for the wheel
   */
  public void setRimColor(int rimColor) {
    this.rimColor = rimColor;
    setupPaints();
    if (!isSpinning) {
      invalidate();
    }
  }

  /**
   * @return the base spinning speed, in full circle turns per second
   * (1.0 equals on full turn in one second), this value also is applied for
   * the smoothness when setting a progress
   */
  public float getSpinSpeed() {
    return spinSpeed / 360.0f;
  }

  /**
   * Sets the base spinning speed, in full circle turns per second
   * (1.0 equals on full turn in one second), this value also is applied for
   * the smoothness when setting a progress
   *
   * @param spinSpeed the desired base speed in full turns per second
   */
  public void setSpinSpeed(float spinSpeed) {
    this.spinSpeed = spinSpeed * 360.0f;
  }

  /**
   * @return the width of the wheel's contour in pixels
   */
  public int getRimWidth() {
    return rimWidth;
  }

  /**
   * Sets the width of the wheel's contour
   *
   * @param rimWidth the width in pixels
   */
  public void setRimWidth(int rimWidth) {
    this.rimWidth = rimWidth;
    if (!isSpinning) {
      invalidate();
    }
  }

  public interface ProgressCallback {
    /**
     * Method to call when the progress reaches a value
     * in order to avoid float precision issues, the progress
     * is rounded to a float with two decimals.
     *
     * In indeterminate mode, the callback is called each time
     * the wheel completes an animation cycle, with, the progress value is -1.0f
     *
     * @param progress a double value between 0.00 and 1.00 both included
     */
    public void onProgressUpdate(float progress);
  }

  static class WheelSavedState extends BaseSavedState {
    //required field that makes Parcelables from a Parcel
    public static final Parcelable.Creator<WheelSavedState> CREATOR =
        new Parcelable.Creator<WheelSavedState>() {
          public WheelSavedState createFromParcel(Parcel in) {
            return new WheelSavedState(in);
          }

          public WheelSavedState[] newArray(int size) {
            return new WheelSavedState[size];
          }
        };
    float mProgress;
    float mTargetProgress;
    boolean isSpinning;
    float spinSpeed;
    int barWidth;
    int barColor;
    int rimWidth;
    int rimColor;
    int circleRadius;
    boolean linearProgress;
    boolean fillRadius;

    WheelSavedState(Parcelable superState) {
      super(superState);
    }

    private WheelSavedState(Parcel in) {
      super(in);
      this.mProgress = in.readFloat();
      this.mTargetProgress = in.readFloat();
      this.isSpinning = in.readByte() != 0;
      this.spinSpeed = in.readFloat();
      this.barWidth = in.readInt();
      this.barColor = in.readInt();
      this.rimWidth = in.readInt();
      this.rimColor = in.readInt();
      this.circleRadius = in.readInt();
      this.linearProgress = in.readByte() != 0;
      this.fillRadius = in.readByte() != 0;
    }

    @Override public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeFloat(this.mProgress);
      out.writeFloat(this.mTargetProgress);
      out.writeByte((byte) (isSpinning ? 1 : 0));
      out.writeFloat(this.spinSpeed);
      out.writeInt(this.barWidth);
      out.writeInt(this.barColor);
      out.writeInt(this.rimWidth);
      out.writeInt(this.rimColor);
      out.writeInt(this.circleRadius);
      out.writeByte((byte) (linearProgress ? 1 : 0));
      out.writeByte((byte) (fillRadius ? 1 : 0));
    }
  }
}
这是搭好布局的截图:


如果我们需要加载网络图片时,首先开始加载图片时会显示一个刚开始类似图片的背景图和加载的圈圈效果。

如果加载失败则在失败那里加载圆圈失败后显示一个失败的图出来。

这里Relativelayout中的id为progress_wheel_layout这个布局就是我们设置在加载圆圈时的背景图。

下图为加载失败时的截图显示:




作者:llixiangjian 发表于2016/11/28 23:57:18 原文链接
阅读:22 评论:0 查看评论

android M Launcher之数据库实现

$
0
0

前面一系列文章我们分析了LauncherModel的工作过程,它会把数据绑定到桌面上。从今天开始我们来分析下Launcher的数据来源即Launcher数据库的实现。

一个完整的数据库实现都应该包括两方面的内容,第一是数据库实体SQLiteOpenHelper的实现,第二是数据库ContentProvider的实现。数据库的实体包含了数据库实体以及相关的操作,ContentProvider负责数据库内容的访问接口实现。

1、Launcher数据库的实现

  private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
        private final Context mContext;
        @Thunk final AppWidgetHost mAppWidgetHost;
        private long mMaxItemId = -1;
        private long mMaxScreenId = -1;

        private boolean mNewDbCreated = false;

        @Thunk LauncherProviderChangeListener mListener;

        DatabaseHelper(Context context) {
            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
            mContext = context;
            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);

            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
            // the DB here
            if (mMaxItemId == -1) {
                mMaxItemId = initializeMaxItemId(getWritableDatabase());
            }
            if (mMaxScreenId == -1) {
                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
            }
        }
        ...
     }

相信大家对SQLiteOpenHelper 的构造方法都比较了解我们主要看下Launcher相关的

    if (mMaxItemId == -1) {
        mMaxItemId = initializeMaxItemId(getWritableDatabase());
      }
    if (mMaxScreenId == -1) {
         mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
    }

通常每一个数据库表都包含一个可以自增长的id字段,但Launcher比较特殊,id字段只作为数据库表的主键存在,因此,我们每一次在桌面上增加一个组件,都需要在当前最大的id号上加1,以做新的id号这就是mMaxItemId 。

在Launcher3之前。Launcher的桌面页数是固定的,随着Launcher3的到来,桌面的页数已经修改为可以动态增加了。如果当前桌面上已经没有额外的空间来加载新增的桌面组件,Launcher3将会根据当前最大的桌面页ID再增加一个桌面页。initializeMaxScreenId方法就是用来获取最大桌面页数的。

上面我们在构造函数中创建了Launcher的数据库,下面我们将在oncreate中创建表。代码如下:

@Override
        public void onCreate(SQLiteDatabase db) {
            if (LOGD) Log.d(TAG, "creating new launcher database");

            mMaxItemId = 1;
            mMaxScreenId = 0;
            mNewDbCreated = true;

            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
            long userSerialNumber = userManager.getSerialNumberForUser(
                    UserHandleCompat.myUserHandle());

            db.execSQL("CREATE TABLE favorites (" +
                    "_id INTEGER PRIMARY KEY," +
                    "title TEXT," +
                    "intent TEXT," +
                    "container INTEGER," +
                    "screen INTEGER," +
                    "cellX INTEGER," +
                    "cellY INTEGER," +
                    "spanX INTEGER," +
                    "spanY INTEGER," +
                    "itemType INTEGER," +
                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
                    "isShortcut INTEGER," +
                    "iconType INTEGER," +
                    "iconPackage TEXT," +
                    "iconResource TEXT," +
                    "icon BLOB," +
                    "uri TEXT," +
                    "displayMode INTEGER," +
                    "appWidgetProvider TEXT," +
                    "modified INTEGER NOT NULL DEFAULT 0," +
                    "restored INTEGER NOT NULL DEFAULT 0," +
                    "profileId INTEGER DEFAULT " + userSerialNumber + "," +
                    "rank INTEGER NOT NULL DEFAULT 0," +
                    "options INTEGER NOT NULL DEFAULT 0" +
                    ");");
            addWorkspacesTable(db);

            // Database was just created, so wipe any previous widgets
            if (mAppWidgetHost != null) {
                mAppWidgetHost.deleteHost();

                /**
                 * Send notification that we've deleted the {@link AppWidgetHost},
                 * probably as part of the initial database creation. The receiver may
                 * want to re-call {@link AppWidgetHost#startListening()} to ensure
                 * callbacks are correctly set.
                 */
                new MainThreadExecutor().execute(new Runnable() {

                    @Override
                    public void run() {
                        if (mListener != null) {
                            mListener.onAppWidgetHostReset();
                        }
                    }
                });
            }

            // Fresh and clean launcher DB.
            mMaxItemId = initializeMaxItemId(db);
            setFlagEmptyDbCreated();

            // When a new DB is created, remove all previously stored managed profile information.
            ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext);
        }

在这里 首先创建了一张名为favorites的表,还是看上面的代码吧 ,就不在复制一份了,这个表主要记录桌面组件自身的信息以及在桌面上的属性信息。我们看下这张表的主要字段含义。

  • _id 这是每个桌面组件在favorites表中的唯一标示,也是favorites表的主键,这个特殊的地方就是它不是自增长类型,所有只能手动确保这个字段的唯一性。

  • title 表示应用程序快捷方式的标题,

  • intent 只有在桌面上摆放的是应用程序快捷方式的时候,该字段才会有值,别的情况下它是没有值的,因为应用程序的快捷方式涉及到应用程序的启动,而对于启动应用程序而言,Intent是至关重要的。

  • container 用于标示一个快捷方式处于什么样的容器中,目前Launcher提供了两种不同的容器,分别是热键区CONTAINER_HOTSEAT,取值为-101 和桌面容器CONTAINER_DESKTOP 取值为-100.

  • screen 用于标示快捷方式所在的屏幕ID。

  • cellX cellY 这两个整型字段用来表示桌面组件在桌面容器中的位置信息。举个栗子 如果Launcher将桌面区域分为5X6,那么cellX的取值范围就是0-4 cellY取值范围就是0-5.

  • spanX spanY 这两个字段用来表示桌面组件所占据的桌面范围信息,以cellX和cellY中的栗子说明 spanX取值范围是1-5 spanY的取值范围是1-6 如果X轴和Y轴取值大于或者小于这个范围,那么组件是无法加载到桌面上的。

  • itemType 用来表示快捷方式的类型。

    • ITEM_TYPE_APPLICTION值为0 意味着这个快捷方式来自应用程序
    • ITEM_TYPE_SHORTCUT 值为1 意味着这个快捷方式来自用用程序创建的快捷方式 比如联系人的快捷方式
    • ITEM_TYPE_FOLDER 值为2 意味着这个组件是一个文件夹
    • ITEM_TYPE_LIVE_FOLDER 值为3 意味着这个组件为一个实时文件夹
    • ITEM_TYPE_APPWIDGET 值为4 意味着这个组件是一个桌面小部件
    • ITEM_TYPE_WIDGET_CLOCK 值为1000 意味着这个组件是一个时钟桌面小部件
    • ITEM_TYPE_WIDGET_SEARCH 值为1001 意味着这个组件是搜索桌面小部件
    • ITEM_TYPE_WIDGET_PHOTO_FRAME 值为1002 意味着这个组件是相册桌面小部件
  • appWidgetId 该字段在favorites中被定义为不能为空并且默认值是-1的整型字段 桌面上除了可以加载不同的应用程序快捷方式以及文件夹等常见的桌面组件外,还可以加载应用程序提供的桌面小部件 这也是Android的特色之一,而桌面小部件主要依赖AppWidgetHost才能运行,每个应用程序都可以创建自己的AppWidgetHost来加载其他或者本应用提供的桌面小部件,每一个桌面小部件在其加载的AppWidgetHost中都被赋予了一个ID,来标示这个桌面小部件,appWidgetId 就是为了保存Launcher创建的AppWidgetHost中某一个桌面小部件的ID 如果桌面加载的并非小部件这个id将是-1 否则为大于或者等于0的值。

  • iconType 表示当此快捷方式需要图标的时候,可能需要保持的图标信息。

  • iconPackage iconResource : iconPackage 描述了图标来用的应用程序包名,iconResource 记录了该资源的ID号

  • icon 用于保持图片的实体

  • uri 当桌面的快捷方式为一个网页链接的时候,这个字段将会保持这个链接的地址否则为null

  • profileId 当前桌面组件所属的用户ID

创建好数据表后,Launcher需要在第一次启动或者数据库被清理的情况下创建页面配置信息表,这张表的目的是保存当前桌面中包含的桌面页的信息

private void addWorkspacesTable(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
                    LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
                    ");");
        }

以上就是Launcher数据库的创建过程。

2、接下来我们看下Launcher的ContentProvider

Launcher的数据库操作都封装在LauncherProvider中,我们在做应用程序开发的时候要对外提供数据,都是使用ContentProvider对数据进行一次包装,然后通过它对外提供数据,这是因为数据库文件往往被创建在应用程序的私有空间,通过ContentProvider可实现跨进程间的访问。Launcher也采用了这种方式。

温馨提示: 每一个ContentProvider的创建都需要比应用程序创建的更早,当在
应用程序清单文件中配置了ContentProvider节点的时候,当应用程序第一次启动
时,框架层就回先将此ContentProvider创建并发布出去。主要就是调用了
ContentProvider的OnCreate方法

Launcher也是遵循这个原则的

 @Override
    public boolean onCreate() {
        final Context context = getContext();
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        mOpenHelper = new DatabaseHelper(context);
        StrictMode.setThreadPolicy(oldPolicy);
        LauncherAppState.setLauncherProvider(this);
        return true;
    }

这里应该都好理解。
通过ContentProvider进行数据库操作都需要通过适当的URI,并配以不同的条件,Launcher的数据库提供者提供了一个专门用于保存并确保这些信息合法的类SqlArguments。
它有三个成员变量

  //需要查询的表名
  public final String table;
  //SQL的查询条件
  public final String where;
  //保存了where条件中所需的参数
  public final String[] args;

知道了成员变量的含义这个函数就很好理解了 这里就不在分析了。

准备知识都完成后 我们继续看下LauncherProvider的增删查改

  • 查询
 @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {

        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(args.table);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
        result.setNotificationUri(getContext().getContentResolver(), uri);

        return result;
    }

这里需要注意的是,在Launcher的Provider创建的时候创建了mOpenHelper实例,当需要对数据库进行操作的时候,需要从中获取数据库的实例,只有通过这个实例才能进行查询操作。

  • 修改
  @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    //更新参数:表名 where条件以及条件参数的设置
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        addModifiedTime(values);
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.update(args.table, values, args.where, args.args);
        //更新通知
        if (count > 0) notifyListeners();

        reloadLauncherIfExternal();
        return count;
    }
  • 增加
 @Override
        public Uri insert(Uri uri, ContentValues initialValues) {
            SqlArguments args = new SqlArguments(uri);

           ...

            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
            addModifiedTime(initialValues);
            final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
            //插入成功rowId 是大于0的
            if (rowId < 0) return null;
            //如果插入成功则依据输入的URI为基础拼接上返回的id形成新的URI
            uri = ContentUris.withAppendedId(uri, rowId);
            notifyListeners();

            if (Utilities.ATLEAST_MARSHMALLOW) {
                reloadLauncherIfExternal();
            } else {
                // Deprecated behavior to support legacy devices which rely on provider callbacks.
                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
                if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
                    app.reloadWorkspace();
                }

                String notify = uri.getQueryParameter("notify");
                if (notify == null || "true".equals(notify)) {
                    getContext().getContentResolver().notifyChange(uri, null);
                }
            }
            return uri;
    }
  • 删除
 @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.delete(args.table, args.where, args.args);
        if (count > 0) notifyListeners();

        reloadLauncherIfExternal();
        return count;
    }

好了以上就是Launcher数据库的实现了,当然Launcher除了提供这些常用的访问方式外,还在内部提供了一些接口工具,以便Launcher的其他组件可以方便的使用Launcher数据库功能。这些比较零散的知识就需要大家在实际开发中去分析了。

作者:asd1031 发表于2016/11/29 0:03:03 原文链接
阅读:112 评论:0 查看评论

iOS 开发 网络编程详解之TCP&UDP

$
0
0

网络通信三要素

这里写图片描述

  • IP地址(主机名)
  • 端口号
  • 传输协议

IP地址(主机名)

本地回环地址:127.0.0.1 主机名:localhost
每台计算机都有一个 127.0.0.1
如果 127.0.0.1 ping 不通,说明网卡不工作
如果本机地址 ping 不通,说明网线坏了

端口号

用于标示进程的逻辑地址,不同进程的标示
有效端口:0~65535
其中 0~1024由系统使用或者保留端口
开发中不要使用 1024 以下的端口
注意 : 跟HTTP相关的端口一定是80.服务器上有个进程是专门处理HTTP请求的,端口号是80.

传输协议

  • TCP(传输控制协议)
    相当于打电话,必须先建立好链接才能传输数据.
    HTTP协议底层是基于TCP/IP协议的.
  • UDP(数据报文协议)
    相当于发电报,不用关心对方是否能够收到.不太安全.

HTTP网络传输协议在传输层选择的是TCP/IP协议

这里写图片描述

UDP(User Datagram Protocol:用户数据报协议)

  • 只管发送,不确认对方是否接收到
  • 将数据源和目的封装成数据包中,不需要建立连接,是不可靠协议
  • 每个数据报的大小限制在64K之内,速度快
  • 应用场景:多媒体教室/网络流媒体 / 视频实时共享

TCP(Transmission Control Protocol:传输控制协议)

  • 建立连接,形成传输数据的通道
  • 在连接中进行大数据传输(数据大小不受限制)
  • 通过三次握手完成连接,四次分手结束连接,是可靠协议,安全送达
  • 必须建立连接,效率会稍低,TCP协议的传输速度比UDP协议慢
  • 提供超时重发,丢弃重复数据,检验数据,流量控制等功能

TCP三次握手建立连接

  • 要想明白Socket连接,先要明白TCP连接。手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。
  • 当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。
  • TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
  • ACK表示Acknowledgment Number字段有意义
    PSH表示Push功能,RST表示复位TCP连接
    SYN表示SYN报文(在建立TCP连接的时候使用,发起一个新连接)
    FIN表示没有数据需要发送了(在关闭TCP连接的时候使用).
  • 各个状态的意义如下:
    LISTEN - 侦听来自远方TCP端口的连接请求;
    SYN-SENT -在发送连接请求后等待匹配的连接请求;
    SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;
    ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;
    FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
    FIN-WAIT-2 - 从远程TCP等待连接中断请求;
    CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
    CLOSING -等待远程TCP对连接中断的确认;
    LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
    TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;
    CLOSED - 没有任何连接状态;

这里写图片描述

第一次握手:建立连接时,客户端发送SYN包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到SYN包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进 入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 连接状态,完成三次握手,客户端与服务器开始传送数据.

握手过程中传送的包里”不包含数据 “,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。

TCP四次挥手断开连接

由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN 来终止这个方向的发送通道。收到一个 FIN 只意味着这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
这里写图片描述
( 1 )客户端 A 发送一个 FIN ,用来关闭客户 A 到服务器 B 的数据传送。
( 2 )服务器 B 收到这个 FIN ,它返回一个 ACK ,确认序号为收到的序号加 1 。和 SYN 一样,一个FIN 将占用一个序号。
( 3 )服务器 B 关闭与客户端 A 的连接,发送一个 FIN 给客户端 A 。
( 4 )客户端 A 发回 ACK 报文确认,并将确认序号设置为收到序号加 1 ,客服端关闭与服务器的连接。

拓展

  1. 为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?

    • 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一 个报文里来发送。
    • 但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未 必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文 和FIN报文多数情况下都是分开发送的。
  2. 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
    因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED 状态(就好比从SYN_SENT 状态到ESTABLISH 状态那样),但是我们必须假想网络是不可靠的,你无法保证你(客户端)最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK 状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT 状态的作用就是用来重发可能丢失的ACK报文。

  3. 关闭TCP连接一定需要4次挥手吗?

    • 不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源)
    • 我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的
作者:kuangdacaikuang 发表于2016/11/29 0:09:45 原文链接
阅读:22 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>