Glide源码看点-图片pre scale

Glide是如何根据View大小对图片进行缩放优化呢 (pre scale)

我们都知道图片实际大小和实际显示的区域一般不能是一样的,所以如果图片尺寸本身很大但是展示区域很小的话就会,如果已完整尺寸对图片进行加载就会效率很低浪费内存,加载速度慢。所以一般我们会根据图片实际需要显示的实际大小对图片进行缩放。
Android内置了inSampleSize方法来对图片进行缩放, Glide抽象出了一个类叫Downsampler.class来根据期望的图片大小和缩放策略进行向下采样。所以关键点变为了Glide如何知道我们的View实际需要的显示区域。下面将通过一个性能问题的debug故事分析glide源码

Wrap Content引发的性能问题“血案”

一个类似聊天信息的界面,如果列表里有4,5张图片后,滑动就会变得异常卡顿。screen_shoot

诊断

打开Android Monitor抓取性能Trace,发现咦咋性能消耗最大的是Glide decode Bitmap。
performance
感觉不应该啊,我图片虽然可能很大但是实际显示区域不大,Glide在显示前应该已经会根据ImageView的实际显示效果进行了缩放采样(DownSample)的优化不应该产生卡顿啊。
**等等****
难道缩放采样出现了问题?

我们根据Glide的调用链找分析。

GlideApp.with(context).load(imgUrl).into(target)

RequestBuilder的into方法其实是调用管理图片加载请求的RequestTrack里的track方法最后调用到了具体Request的begin方法。
Step1:
SingleRequest.run()方法 -> ViewTarget.getSize()

如果开发者有制定大小OverrideSize就会直接采用该尺寸,如果没有就去ViewTarget里去拿size
ViewTarget.getSize -> sizeDeterminer.getSize(cb);

ViewTarget里会根据SizeDeterminer去获取TargetView的具体尺寸。如果能直接从View里读取到具体的尺寸就直接用。

如果读取不到就用PreDrawListener在layout完成后去监听view的实际尺寸:
observer.addOnPreDrawListener(layoutListener);
关键点来了,去看添加的preDrawListener,里面有个特殊的if语句,如果view定义了WRAP_CONTENT, Glide就无法判断到底使用什么大小去preScale图片,所以它取了屏幕的最大宽度作为图片的加载大小。(不取图片的Target.SIZE_ORIGINAL的原因是可能图片会很大,远远大于屏幕尺寸)
SizeDeterminerLayoutListener:
`

  if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
    if (Log.isLoggable(TAG, Log.INFO)) {
      Log.i(TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of"
          + " this device's screen dimensions. If you want to load the original image and are"
          + " ok with the corresponding memory cost and OOMs (depending on the input size), use"
          + " .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set"
          + " layout_width and layout_height to fixed dimension, or use .override() with fixed"
          + " dimensions.");
    }
return getMaxDisplayLength(view.getContext());
  }

`

元凶

看到这里刚才的性能问题迎刃而解,虽然我们界面里的ImageView实际显示区域很小(maxWitdh和maxHeight限制的),但是Glide不知道。一看我们list item的布局果然如此


所以在列表里的每一张图片都是以手机的最大宽度去加载的,所以decode bitmap才花了巨大的时间。

所以Fix这个问题只需要,在加载图片前设置好需要的加载宽度就可以解决啦。

总结

图片加载的一个重要手段是,“按需显示”更具实际需要显示的图片大小进行预缩放(Pre Scale), PreScale要根据图片的显示大小和图片本身实际大小来计算inSampleSize来进行。Glide在处理Wrap Content的ImageView的时候,会不知道如何进行PreScale需要指定好具体显示的尺寸,不然可能会引发性能问题。

Kotlin coroutine(一)异步回调的故事

Part 1, Motivation

在前端的世界里,为了保证UI的流畅性,耗时的操作都应该丢到异步任务里去。但是传统的异步回调却在实际工程里带来巨大的麻烦。

* 当多个异步任务需要组合执行的时候,嵌套起来可读性,维护性很差 ( *Callback Hell 回调地狱* )
* 界面销毁时,异步任务结束后的回调要进行判断,不然会造成异常

下面一个例子展示一个常见的开发场景下的问题:
同步用户书架,并获取最新章节标志,流程如下:

* 从数据库查询用户的本地数据
* 从服务器拉取用户云端备份数据
* 两份数据去重合并
* 更具合并后的数据请求服务端拉取最新章节信息

传统异步回调式写法:

这样的代码异常难读且非常容易出bug
但是如果代码可以改为类似如下的代码呢? 瞬间可读性,强了数倍。

val localData = fetchUserBookFromDB()
val remoteData = fetchUserBookFromRemote()
val mergeData = mergeData(localData)
val result = fetchBookUpdate(mergeData)

Part 2, 异步问题的改进史

Future/Promise/RxJava

如果每个异步函数都要传递一个callback,来处理问题很容易造成嵌套的结构,那有没有办法回调扁平化处理呢?更具这样的思路,于是就产生了promise和Future。
其实他们都是相当于封装了一个统一的类作为回调把回调从参数列表里移除并做为返回值包装了起来,就可以用来进行链式调用。
Before:

fun requestSomethingAsync(callback: Callabck<Data>)

requestSomethingAsync(object: Callback {
    override onSuccess() {
        doAnotherThingAsync(...)
    }
    override onFailed() {
    }
})

After:

 fun requestSomethingAsync():Promise<Data> {}

 requestSomethingAsync()
    .then(res -> {})
    .catch(err -> {})

async/await

链式调用虽然解决了层级嵌套的问题,但是仍然不是很优雅(想想RxJava那么多种操作符)。与是有了async和await关键字的概念,它可以把异步任务改为同步的方法进行编写。

suspend fun fetchBookUpdateInfo() {
    val localData = fetchUserBookFromDB().await()
    val remoteData = fetchUserBookFromRemote().await()
    val mergeData = mergeData(localData).await()
    val result = fetchBookUpdate(mergeData).await()
}

Part 3, Kotlin coroutine基本概念

coroutine Builder

如字面上的意思,这写类提供创建coroutine的方式
launch, async, runningBlock 等等

  • launch: 按coroutine作者Roman Elizarov说的那样,就像调用new Thread开启一个新的线程做任务,属于Fire and Forget(用完就走,即不用操心任务结果成功失败的那种) 它会立即返回一个Job,Job里拥有如下这样类似Thread的方法。
  • async: 返回的是Deferred,Deferred继承与Job,比一般Job多的是

下面是一个栗子:

coroutine context

coroutine里运行的上下文。规定运行使用的环境是什么。这里和RxJava里决定使用什么类型的线程池一样的概念。
分为Main,IO,Default,Unconfine。
Main就是android里的UI线程,IO是一个线程池使用与做IO操作。
给coroutine制定不同的context可以大幅度的简化UI和取数据的逻辑。
下面我改写上面的栗子:

我们在UI事件上进行对View的操作,在load数据的时候使用的IO的context来进行。

suspend关键字

suspend可以标志这个方法会时当前运行时挂起。suspend函数只能在coroutine里运行。

预告

改用async/await的编程形式大大简化了异步复杂的交互逻辑。并且对错误处理很友好(可以直接对catch)。下一篇将会简单介绍Coroutine背后实现的一些原理。

如何实现Root管理(一)关于ROOT的几个问题

概述

本文将简单介绍CM里对Root权限的开放,关闭,对应用su授权管理进行介绍。内容将涉及init过程,System property的加载,AppOpsManager,Selinux的相关内容。

流程概述:

* 如何让usr版本拥有su的方法:在编译时把su打包到固件里

* 如何实现su的开关:我们期望在init.rc里添加一个service来实现开关su的权限。可以通过修改su.c,修改init相关

* 如何实现应用su的权限管理:修改AppOpsManager来进行管理

理解Root用户,UID和GID

在Linux里用户种类很简单,只有Root和非Root两种。每个进程都有自己的ID(UID),用户组ID(GID),而Root用户的UID和GID都是0。在Android里,每个应用是一个独立的用户运行在独立的虚拟机里,所以应用权限的大小是由它用户的身份决定的。

名称 功能
AID_ROOT 0 ROOT用户
AID_SYSTEM 1000 系统
AID_SHELL 2000 shell用户
AID_APP 10000 第一个app user

一般UID 10000以前都是系统预留用户ID,更多完整的定义可以查看 system/core/android_filesystem_config.h

为什么user版本的adb不能Root

我们可以先查看不同版本下adbd进程的信息,观察不同。

user版本下adb的运行信息

eng,userdebug下的adb

可以观察到在user版本下adb的用户是AID_SHELL,而在userdebug版本下adbd的用户是Root用户。所以这就导致了user版本下无法root。

Note,带root的shell的uid和gid虽然是0,但是它的安全上下文(context)u:r:shell:s0还是shell,所以从shell里生出的进程还是被shell的context所限制。之前android4.4的时候,selinux没有开启的时候。你如果从shell生出来,你就是root用户组了可以胡作非为了。

为什么现在android要root变得艰难

Android 4.3以后,system分区被挂起为nosuid,任何zygote生出来的进程都不能进行setuid的操作。Selinux在Android 5.0后默认开启为enforce模式了,很多操作都受到限制,在selinux打开的情况下修改root权限,用寸步难行来形容真不为过。

su.c

su可以简单的理解为一个类似命令行的程序,它的存放路径为android/platform/system/extras/su ,在编译的时候这个su会被编译为二进制文件存储在系统镜像里system/xbin下,这个文件只有在userdebug,eng下才会编译到系统镜像里,所以user版本的固件是不能调用su。

int main(int argc, char** argv) {
        uid_t current_uid = getuid();
        // 对调用su的身份做检查
        if (current_uid != AID_ROOT && current_uid != AID_SHELL) 
            error(1, 0, "not allowed");
        // 省略...
}

这也是其他进程无法调用su的原因,如果注释掉这一段su就不会对用户id进行检查了(风险很大这样做)。

如何打包su.c进入usr版本

观察su文件夹下的mk文件

发现大量debug的tag,所以在user版本下它不会被打包到固件里。需要把整个TAG的值改为optional。不过我记得当初我去掉以后还是没有打包进去,估计是还有其他地方还有限制。我是修改了产品的mk保证在编译时一定编译这个模块才行的

PRODUCT_PACKAGES += \
    su

Fun facts with HTTP

当输入一个网址后的事….

HTTP是一种不保存状态的协议(stateless)

HTTP协议自身不对请求和响应之间的通信协议状态进行保存。这样的设计是为了保证协议的简单高效。

Cookie

但是随着Web的发展,越来越多的需求需要记录状态,于是引入了Cookies的设计。Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。Cookie就像是某种token,客户端一份,服务器一份,下次通信时再互相验证。

HTTP管线化(PipeLine)

HTTP是基于TCP的应用嘛,所以早期的HTTP每进行一次HTTP通信,就开断一次TCP连接。我们知道TCP要经过三次握手才能通信。随着web的复杂,这样频繁开断很浪费资源。针对这个问题于是提出了HTTP keep-live方法,其特点是如果任意一端没有明确提出断开连接,则保持TCP连接状态。并且从前必须等到上一个请求响应才能发起下一个请求,而又了管线化技术可以连续请求啦。

压缩传输技术

传送数据压缩才能更快的传输啊,android 2.3以后对HttpUrlConnection进行了gzip压缩的支持

 

 

参考

《图解HTTP》作者: 【日】上野宣 出版社: 人民邮电出版社

 

Volley源码分析

引言

Volley是Google在2013I/O大会发布,如其名它简洁高效,之后几乎所有人都在用它。今天我们来分析一下它。

整体结构及其策略

RequestQueue:请求队列,所有的网络请求将在这PriorityQueue处理。

Dispatcher :负责请求的分发,一个负责发起缓存请求,一个负责发起网络请求

HttpClientStack和HurlStack :是真正发起网络请求的地方,这里是针对不同的Android版本分别对AndroidHttpClient(已废弃,适用于API 9以下)和HttpUrlConnection的封装

策略:

请求加入到RequestQueue里,然后被CacheDispatcher取出请求检查是否在Cache里,如果在取出数据,传回给主线程ok结束。如果Cache没有命中或者cache过期就把请求放入网络请求队列里,然后发起网络请求结束。

源码详细分析策略

分析源码当然要从我们平时的使用方法作为入口开始入手

java

Volley.newRequestQueue(Context context)

 

调用这个静态方法最后会掉到(省略部分代码)

 public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

      // 根据不同Android版本新建不同的网络请求处理栈
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);
        // 新建RequestQueue,并调用start方法
        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    }

跟着上面RequestQueue的start方法

  public void start() {
        stop();  // 确保一批Dispatcher在活动
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // 根据Dispatcher池子大小来创造Dispatcher
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

可以看到Dispatcher在RequestQueue调用start方法的时候被创建,且在这个时候分别开启他们的线程。因为Dispatcher实际上是Thread,所以start后我们直接分析它的run方法

CacheDispatcher的run方法(代码有所省略):

  @Override
    public void run() {
    // 这里设置线程的优先级很重要,后面会说明
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        Request<?> request;
        while (true) {
           // ...
            try {
                // 尝试从队列里取出一个请求
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                // 如果Request被取消,直接结束
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // 尝试读取缓存
                Cache.Entry entry = mCache.get(request.getCacheKey());
                // 缓存为空,加入网络请求队列
                if (entry == null) {
                    request.addMarker("cache-miss");
                    mNetworkQueue.put(request);
                    continue;
                }

                // 缓存过期,也加入网络请求队列
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // Cache命中,读取缓存
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 不需要刷新的叫完全命中,直接投递结果
                    mDelivery.postResponse(request, response);
                } else {
                    // 有缓存但是需要刷新的叫Soft-expired cache hit,我们投递结果,但是也把请求加入网络请求队列
                    // ...
                 }
        }
    }

如何传回数据到主线程呢?很简单就是有一个持有主线程Looper的Handler了就行啦。这个类是ExecutorDelivery,就不分析了。

Volley ImageLoader 分析

要分析ImageLoader先要提前说一下其中的一个数据封装类ImageContainer它持有关于网络图片的所有信息:

public class ImageContainer {

        private Bitmap mBitmap;
        // 图片请求结果的listener
        private final ImageListener mListener;

        // 图片请求缓存key
        private final String mCacheKey;

        private final String mRequestUrl;
}

这个ImageLoader设计最棒的地方,不仅在于可以读取缓存图片,还可以对重复的请求进行过滤避免发起多次重复的请求(这种重复请求的情况,在Listview有图片时上下滚动列表时尤为明显)

那么它是如何实现这一功能的呢?答案也很简单它用了一个Map来对请求作缓存。

// InFlightRequest: 这个Map用来缓存还正在请求中的请求队列,用这个队列来实现避免重复请求,当请求结束后移除请求
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
            new HashMap<String, BatchedImageRequest>();

下面我们来看ImageLoader的get方法,通常我们来获取图片的方法(代码有所省略):

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 查看是否有缓存
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 返回缓存数据
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // 没有缓存,准备请求
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // 检查一下请求是否已经请求已经在处理
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 如果已经在了,我们只在这个请求中添加我们的监听器
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // 请求没有正在处理,加入网络请求队列,别再InFlight里记录
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

小知识

BlockingQueue

它与一般的Queue不同的是多了对两个操作的支持,等到队列不为空的时候取元素,和等队列不为全满的时候放入元素。(中文好拗口,看英文)A Queue that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.

Android 自己使用线程时的注意事项

一个重要的知识点是,当一个线程被创建的时候,它将拥有和同一组线程里同样的优先级。

这意味着入股,你在UI thread里打开一个线程,如果不设置恰当的优先级,这个worker thread将和UI thread享受同样的优先级,这样可能导致worker thread和UI thread抢资源导致卡顿。前面你也可能注意到了,在Volley新建Dispatcher的时候是在UI thread,所以它把线程的优先级设置为了THREAD_PRIORITY_BACKGROUND。

android里设置priority是从负数到正数的,优先级递减的。如何理解记忆呢,可以想成这个priority的值表达了这个线程对他人的友好程度,友好程度越低代表它的时间越珍贵。

正常情况下我们希望是这样的

(在AsyncTask里和IntentService里默认是这样实现的)

参考

* [Google I/O 2013 - Volley: Easy, Fast Networking for Android](https://www.youtube.com/watch?v=yhv8l9F44qo)
*
* [Volley 源码解析](http://codekk.com/blogs/detail/54cfab086c4761e5001b2542)
* [BlockingQueue java doc](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html)

ArrayMap VS HashMap

在Java里面用Map里面的HashMap作为容器我们使用的频率很高,而ArrayMap是Android api提供的一种用来提升特定场和内存使用率的特殊数据结构。今天我就写一篇博客记录一下。

HashMap

QQ截图20150823235946

Java库里的HashMap其实是一个连续的链表数组,通过让key计算hash值后插入对应的index里。当hash值发生碰撞时,可以采用线性探测,二次hash,或者后面直接变成链表的结构来避免碰撞。因为hash的值不是连续的,所以hashmap实际需要占用的大小会比它实际能装的item的容量要大。我们可以看一下HashMap的源码:

  
 public HashMap(int initialCapacity, float loadFactor)   
 {   
     // 初始容量不能为负数  
     if (initialCapacity < 0)   
         throw new IllegalArgumentException(   
        "Illegal initial capacity: " +   
             initialCapacity);   
     // 如果初始容量大于最大容量,让出示容量  
     if (initialCapacity > MAXIMUM_CAPACITY)   
         initialCapacity = MAXIMUM_CAPACITY;   
     // 负载因子必须大于 0 的数值  
     if (loadFactor <= 0 || Float.isNaN(loadFactor))   
         throw new IllegalArgumentException(   
         loadFactor);   
    //....
    // 设置容量极限等于容量 * 负载因子  
     threshold = (int)(capacity * loadFactor);   
     // 初始化 HashMap用于存储的数组  
     table = new Entry[capacity];            // ①  
     init();   
 }   

你会发现它又一个变量叫loadfactor,还有threshold。threshold就是临界值的意思,代表当前HashMap的储存机构能容纳的最大容量,它等于loadfactor * 容量。当HashMap记录存入的item size大于threshold后,HashMap就会进行扩容(resize)。当我们第一次新建一个HashMap对象的时候,默认的容量是16,若你只打算在HashMap里放入3个元素那将浪费至少13个空间。

ArrayMap

ArrayMap是怎么实现节省内存的呢?先放数据结构图:

QQ截图20150824001700

 

他用两个数组来模拟Map,第一个数组存放存放item的hash值,第二数组是把key,value连续的存放在数组里,通过先算hash在第一个数组里找到它的hash index,根据这个index在去第二个数组里找到这个key-value。

在这里,在第一个数组里查找hash index的方法当然是用二分查找啦(binary search)。QQ截图20150824002101

这个数据结构的设计就做到了,有多个item我就分配多少内存,做到了memory的节约。并且因为数据结构是通过数组组织的,所以遍历的时候可以用index直接遍历也是很方便的有没有!但是缺点也很明显,查找达不到HashMap O(1)的查找时间。

当要存储的对象较少的时候(1000以下的时候)可以考虑用ArrayMap来减少内存的占用。

曲线的魔力—贝塞尔曲线(二)

先放效果图!(类似腾讯qq的消息消失动画)上一篇回顾

 

gooey怕了吧,有没有好奇这个动画到底是怎么做的?其实原理超级简单,不要被唬住了。这里就是展现贝塞尔曲线牛逼的时候到了。其实上一篇将贝塞尔曲线只是说了它的原理,但是没有一个形象的比喻,下面我要用一个形象的比喻来解释贝塞尔曲线。控制贝塞尔曲线的过程其实就像是操纵橡皮筋,我们首先把橡皮筋的两头用钉子固定住(这就是确定了start point和end point),然后我们可以用手去拨动橡皮筋中间的部分,这就是贝塞尔曲线另外一个概念控制点(control point),我们可以通过设置不同的控制点产生不同的曲线。cubic

explain

看图其实这个动画就是两个圆形,中间两条曲线由贝塞尔曲线画出来!是不是很简单!但是难点在于,如何控制两条曲线的变化,也就是如何确定贝塞尔曲线的控制点呢?我在这里的实现比较简单,先看下图:

explain

 

控制点如图就是p5和p6,他们分别是线p1p4和p2p3线的中点。每次动态更具圆的位置动态计算控制点的位置就ok啦!

代码我就不讲解啦,很简单:

package com.example.qianlv.gooeyeffect;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;

/**
 * Created by qianlv on 2015/8/16.
 */
public class GooeyView extends View {
    float radius1;
    float radius2;
    RectF rect1;
    RectF rect2;
    Paint paint;
    TimeAnimation animation;
    private float mInterpolatedTime;
    PointF p1;
    PointF p2;
    PointF p3;
    PointF p4;
    PointF p5;
    PointF p6;
    float dx1;
    float dy1;
    float dx2;
    float dy2;

    public GooeyView(Context context) {
        super(context);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(0xff30accf);
        paint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        // 设置两个圆大小
        radius1 = width * 0.13f;
        radius2 = width * 0.1f;

        rect1 = new RectF();
        rect1.left = width*0.05f;
        rect1.top = rect1.left;
        rect1.right = radius1 * 2;
        rect1.bottom = rect1.top + radius1 * 2;

        rect2 = new RectF();
        rect2.left = rect1.right + 200;
        rect2.right = rect2.left + radius2 * 2;
        rect2.top = rect1.centerY() - radius2;
        rect2.bottom = rect2.top + radius2 * 2;
        dx1 = radius1*0.5f;
        dy1 = dx1*1.732f;
        dx2 = radius2*0.5f;
        dy2 = dx2*1.732f;

        p1 = new PointF(rect1.centerX()+dx1,rect1.centerY()-dy1);
        p3 = new PointF(rect1.centerX()+dx1,rect1.centerY()+dy1);
        p2 = new PointF(rect2.centerX()-dx2,rect2.centerY()-dy2);
        p4 = new PointF(p2.x,rect2.centerY()+dy2);
        p5 = new PointF((p1.x + p4.x)*0.5f,(p1.y + p4.y)*0.5f);
        p6 = new PointF((p2.x+p3.x)*0.5f,(p2.y+p3.y)*0.5f);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRGB(255, 255, 255);
        canvas.drawCircle(rect1.centerX(), rect1.centerY(), radius1, paint);
        canvas.drawCircle(rect2.centerX(), rect2.centerY(), radius2, paint);

        // 绘制贝塞尔曲线
        Path curve1 = new Path();
        curve1.moveTo(p1.x,p1.y);
        curve1.quadTo(p5.x,p5.y,p2.x,p2.y);
        curve1.lineTo(p4.x,p4.y);
        curve1.quadTo(p6.x,p6.y,p3.x,p3.y);
        curve1.lineTo(p1.x, p1.y);
        canvas.drawPath(curve1,paint);

        // 每次重绘时移动距离
        rect2.left +=5;
        rect2.right +=5;
        if (rect2.right > getWidth()){
            rect2.left = rect1.right + 30;
            rect2.right = rect2.left + radius2 * 2;
        }

        // 重新计算每个点的位置
        p2.x = rect2.centerX()-dx2;
        p2.y = rect2.centerY()-dy2;
        p4.x = p2.x;
        p4.y = rect2.centerY()+dy2;
        p5.x = (p1.x + p4.x)*0.5f;
        p5.y = (p1.y + p4.y)*0.5f;
        p6.x = (p2.x+p3.x)*0.5f;
        p6.y = (p2.y+p3.y)*0.5f;
    }

    // 用于改变时间
    private class TimeAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            mInterpolatedTime = interpolatedTime;
            invalidate();
        }
    }

    private void startAnimation() {
        animation = new TimeAnimation();
        animation.setDuration(10000);
        animation.setInterpolator(new LinearInterpolator());
        animation.setRepeatCount(Animation.INFINITE);
        animation.setRepeatMode(Animation.REVERSE);
        startAnimation(animation);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }
    private void stopAnimation() {
        this.clearAnimation();
        postInvalidate();
    }
    @Override
    protected void onDetachedFromWindow() {
        stopAnimation();
        super.onDetachedFromWindow();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 重置
        rect2.left = rect1.right + 30;
        rect2.right = rect2.left + radius2 * 2;
        startAnimation();
        return true;
    }
}

参考:

 

 

实习学习记录(一)

偷懒一下,打算写一个小系列记录实习期间学习到的知识点总结一下。

View绘制的流程

View和Activity一样其实也有自己的生命周期(这里的生命周期不是指像Activity一样有onPause,onResume这种一样,这里指代的是View的一辈子所要干的事的一种抽象概念)。不过View的生命周期远比Activity的生命周期简单。View的生命周期里主要做了三件事:Attachment/Detachment,遍历(英文是Traversal,View的树形结构遍历,里面包括了onMeasure,onLayout,onDraw等方法的递归遍历调用),保存恢复数据 。

关于Attachment:就像Fragment要依附于Activity一样它又一个onAttach的方法一样。View也有一个叫onAttachedToWindow的方法。在这个方法里面,我们一般进行资源分配,注册监听器。如果你看ListView的源代码你可以发现,google在这里对Adapter注册了监听器用于监听数据变化。对应的View还有onDetachedWindow,在这里一般把分配的资源释放掉,还有把监听器弄掉。

关于遍历:

View在你看到前系统会对它走如下的流程:

Animate->Measure->Layout->Draw

Measure主要是要确定自己到底要占多少位置,多少大小。比如你要知道match_parent和wrap_content的时候你到底需要多大。Layout就是需要确定 child view的显示位置。Draw就是对view进行绘制。

Measure里新手第一次见到的时候会被好多新东西吓到。我来一个个解说,你先是会发现有一个MeasureSpec的数据结构,这个是对View的宽高,还有测量模式的一个结构封装。可以用MeasureSpec.getSize,MeasureSpec.getMode来进行调用。这里的Mode有三种模式:

  • Exactly:比如你确切的给view一个大小如200dp,还有match_parent,LinerLayout里的weight这种都属于Exactly确切模式
  • AT_MOST:这里是Wrap_content的应用。
  • UNSPECIFIED:不确定模式,一般是在ListView和ScrollView这种内容长度不确定的东西。

这里其实我们再自己自定义控件的时候我们没必要考虑每一种模式的计算情况,因为毕竟是自己使用,你可以假定我们永远只用match_parent啊或者是永远wrap_content,所以你主需要写一种模式的测量方法即可。(当然除非你想确保你的控件的普遍适用性,你可以考虑每一种模式)

onLayout

这里是利用前面测量好的数据(getMeasureWidth和getMeasureHeight)来确认view里的child view的放置位置。这里要注意有意思的一点是,View.getWidth和View.getHeight只有在onLayout完之后才可以使用,不然在之前调用返回的都是0.(这种返回0的情况我想很多人在写自己自定义的ViewGroup还有在自己自定义的Adapter的时候都会遇到)那是不是在view显示过一次之前我们都无法获得view的宽高了呢?并不是呢,我们这里还有一个利器叫onPreDrawListener,这个监听器的触发时间将在onLayout之后和onDraw之前。

Draw:这里面用得最多得就是invalidate。和requestLayout类似,调用它以后会触发View树的遍历,就是对它的parent一直进行递归调用直到到了view root。这里注意invalidate是我在UI线程里调用的函数,如果我在非UI线程的时候需要调用postInvalidate。

状态保存:一般是通过Override,onSaveInstanceState和onRestoreInstanceState实现。

clipRect的使用:如果每次调用invalidate的时候都把整个view重绘,将是很耗性能的事情。我们可以通过规定每次需要重绘的区域(一个矩形Rect)让GPU知道每次我只需要对View的这部分显示进行更新就好。如下面这个例子,在这个自定义View包括了文字还有一个按钮,按钮包括一个进度条显示的功能。若果每次只需要更新的是按钮的进度,而重绘整个view就显得有点浪费。可以我们可以选择每次只更新那个按钮的位置。通过设置canvas.clipRect(Rect clipArea)即可。clip

StaticLayout用于在canvas里绘制文字

这个相当于隐藏的API,在canvas里我们绘文字的时候,drawText方法字符串有多长它就画多长,它不会自动换行。用StaticLayout可以解决这个问题。通过建立一个StaticLayout区域,在里面drawText是会自动换行的。TextView就是基于这个实现的。想实现drawText文字换行还有一招,只是有点麻烦思路如下:获取显示区域大小如DisplayWidth,然后测量用画笔画所需字符串将得到的宽度TextWidth(这里是通过调用Paint.getTextBounds(),或Paint。getTextWidths获得,区别是前者是返回int后者是float。后者更精准)如果TextWidth > DisplayWidth我们就对要显示的字符串进行substring然后在下一行的位置进行显示(下一行的位置也是我们自己计算的,用前面得到的TextBounds来获得getHeight)比较麻烦。代码我就不放了,实现效果大概如下:

QQ图片20150726112820

曲线的魔力——初识贝塞尔曲线

为什么我们需要贝塞尔曲线?

设想一下平时曲线对计算机来说是画出来的呢。无非就是要用一个函数y = f(x)来绘制曲线。但是我们能拿到手就用的函数曲线无非是sin,cos这种函数了,若我们想要更复杂的曲线,获取它的方程将会是很难的。

curve
如何产生这个曲线的方程呢?

这个时候,贝塞尔曲线的作用就来了。

什么是贝塞尔曲线?

贝塞尔曲线(英语:Bézier curve)是电脑图形学中相当重要的参数曲线。是用几个很少的控制点就可以产生平滑复杂的曲线的方法。我们经常在开发中用贝塞尔曲线来绘制曲线,或者控制动画的时间变化(擦除函数)。

下面展示一下贝塞尔曲线算法的绘制过程:

先定义三个点

贝塞尔1

 

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

贝塞尔2

 

根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB = BE:BC

贝塞尔3

 

连接D,E得

贝塞尔4

 

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC。

贝塞尔6如果把这个过程反复的做下去将形成一个曲线!

贝塞尔5

 

wiki上的动图效果如下:(好像要点开才看得到)

Bézier_2_big

 

当然控制点还可以有更多来操作出更复杂的曲线。

在Android里的应用

Android里的Path提供了画贝塞尔曲线的API,下面我来做一个简单介绍。

Path包含贝塞尔曲线的API主要包括,quadTo,cubicTo,rCubicTo,rQuadTo。

其中quadTo是有两个控制点的贝赛尔曲线,cubicTo有三个控制点。

例子:

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

/**
 * Created by Jiaqi Ning on 5/7/2015.
 */
public class PathView extends View {
    Paint paint;

    public PathView(Context context) {
        super(context);

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0xff41be9d);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    //draw the curve
        Path path = new Path();
        path.moveTo(50,50);
        path.cubicTo(50,50,50,300,250,300);
        canvas.drawPath(path,paint);
// draw the control line
        path.reset();
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(2);
        path.moveTo(50,50);
        path.lineTo(50,300);
        path.lineTo(250,300);
        canvas.drawPath(path,paint);

    }
}

curve_screenshot更强大的用法,考完试再写吧 Orz…

参考:

 

 

Java里的异常

 

前言:最近都在准备考试复习都木有这么学习新姿势,临时找了以前看书的记录凑出了这个文章,太罪过了><

感觉自己目前写的程序都是编译器在强制自己要做异常处理,对于怎么样编写优秀的异常处理程序还不算很清楚,下面先整理一下Thinking in java里看到的关于异常的知识吧。异常处理的原则是”只有你知道如何处理的情况下才捕获异常”,其中一个重要目标是将错误处理代码和错误发生地点分离。

分类:分为被检查异常(checked)和不被检查异常(unchecked)。这里的检查指的是否进行编译时期的强制检查。

重新抛出对象fillStackTrace的用法;异常出现后异常对象会被压栈,我们可以通过打印栈里的信息,来获取发生异常的函数调用情况,栈底的元素是调用序列里的第一个方法。这里要注意异常重新抛出的一个问题,有时候我们捕获一个异常后我们并不希望在这处理,只是想像上一层再次抛出,比如像catch(Exception e ) {

throw e; // rethrow the exception

}这样重新抛出的话,若用printStackTrace打印栈元素的话,只会显示最原始抛出异常点的信息而不包括重新抛出点的位置,若要把重新抛出异常的信息也要加入栈里,需要调用fillInStackTrace这个方法把信息压入。

finally的注意和特别用法。finally作为try/catch机制里最后进行处理的措施,就算try里遇到了break和return,finally也还是会执行的。

继承对异常的影响。若子类继承了父类的某个方法,子类可以不抛出父类规定的异常。子类要覆写(Override)父类的某个方法,若原方法定义了某个异常,子类的方法里不能再给这个方法抛出更多异常。

切勿“吞食”掉异常。写异常有时候的确很烦,特别遇到有很多异常嵌套的时候,我们为了应付编译器就只写了一个try/catch在那或者简单的throw出去,这个异常就被吞食了,这不是一个好习惯。最次我们也要把异常打印出来提醒,或者我不知道这里遇到异常应该怎么处理,我就抛出一个RuntimeException结束程序把。