Blog

hirayclay's blog


  • Home

  • Archives

关于RecyclerView的一个有趣的事情

Posted on 2018-01-19

对于RecylcerView ,基本上第一印象就是View重用,但是真的明白怎么重用的吗,最近在写自定义LayoutManager,由此对RecylerView、LayoutManager、ItemAnimator整个之间的关系都比较的熟悉。不过回到标题上来,这个有趣的事情和RV的回收有关。

比如页面上此时显示了前六条Item(第六个Item没有显示全),那么你肯定觉得不把第六条Item全部划进来,第七条就不会调用onCreateViewHolder进行创建;但事实是当我只要向上稍微滑出去一点就会创建第七个Item,这是不是和对RV的回收重用的印象有些矛盾?是的,按照常理,我根本都没有滑出第七个Item,你就创建了,好像不太对。

最后我看了下源码,其实原因比较简单,得先贴一下滑动发生时候调用填充逻辑的方法代码(部分代码):

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
        }

        return start - layoutState.mAvailable;
    }

因为当滑动发生的时候,填充的postion是从第七个Item开始的,所以第七个Item被创建了,但是呢,立马被 recycleByLayoutState(recycler, layoutState)这个方法给回收掉了,并且缓存了起来,毕竟第七个Item在屏幕外,所以被回收了,而且这个while跑了这一次就退出了,到了第七个Item真的出来的时候就直接从缓存里面取出来用了

Note:”scrap” View指的是仍然有效可以直接拿来重用的VH,只是暂时脱离了RV。名字让人很误解。

Gradle的一些小知识(不定期更新)

Posted on 2018-01-05

资源分包

一次不小心点进去sourceSet进去,发现可以定义资源路径的;自己按照main目录下的结构一样创建了一个debug的SourceSet(新建的其他名字的都不行,debug和main可以),然后在module的gradle中加入如下配置就等于是有两个res目录,这样可以让资源分类更清晰

        sourceSets {
            debug {
                res.srcDirs("src/debug/res_debug", "src/debug/res")
            }

        }

记得sync一下

最后生成apk的时候两个sourceSet的东西会合并

Parcelable

Posted on 2018-01-05

这里并不是要仔细说一遍Parcelable,而是看了一些Parcelable的国内博客,发现都是说怎么用。怎么用官方文档就有例子,而是有个点,没有一篇博客说出来(也有可能我看的不仔细?)。
这个疑惑估计你也有过,在使用Parcelable 序列化和序列化的时候都是write read方法调用,但是发现如果有两个相同类型的值,比如有两个int要序列化,我们是不是要调用writeInt两次,然后反序列化的时候调用两次readInt,那么问题来了,反序列化的时候调用readInt怎么就知道是拿到的正确的值,而不会拿反了,毕竟两个int呢。
就这个不大不小的点,一直没看到,当然我也想过,write readInt操作的值的顺序必须一致,不然就会错
写了一小段代码跑了下,确实必须按照顺序来,因为底层就是个指针在挪动,挨个读取,比如取一个int的值,就会挪动4字节,所以顺序错了有时会拿到错的值,甚至拿不到正确的值(读到null直接crash)

    var name: String = ""
    var nickName: String = ""
    var id: Int = 0

    constructor(parcel: Parcel) {
        id = parcel.readInt()
        nickName = parcel.readString()
        name = parcel.readString()
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeString(nickName)
        parcel.writeInt(id)
    }

然后直接崩了

这里有篇歪果仁写的博客,很详细

The End

LinearLayoutManager源码阅读(滚动分析)

Posted on 2017-12-19

实现自定义的通用的LayoutManager,但是卡住了,遂看下Android 官方的几种LayoutManager是如何优雅实现的,大致的以及一些细节都看懂了,但是还是没找到什么好办法解决自己的问题,不如趁着热度把自己的分析过程写下来,也给其他需要的Androider.其实真的要对RecyclerView有个全面的认识,其实LayoutManager、Adapter、动画以及测量流程等细节都是要清楚的,因为虽然说RV给人使用上非常灵活解耦,但是其实内部也是这几者的紧密配合才达到的效果,所以有一点不明白其他地方可能也就会很模糊看不下去。也不细分章节了,就按照滚动的流程来写。至于为什么从滚动开始分析,是因为看源码还是讲究切入点,从RecyclerView的滑动开始是最佳切入点,很直观,要是第一次直接就从onLayoutChildren看起,我觉得看不了几行就得放弃。当然这篇文章的内容很多没说清楚的其实都是onLayoutChildren的一些逻辑,所以都略过了,只关注滑动。

由于自定的LayoutManager如果要(肯定要,不然还定义啥)支持滚动都必须至少重写以下两个方法中的一个,并且返回true,分别表示支持垂直滚动和水平滚动

 public boolean canScrollHorizontally() {
            return true;
        }
 public boolean canScrollVertically() {
            return true;
        }

在发生滚动的时候,会在以下两个方法回调滚动的距离dy/dx

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
            return 0;
        }

    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
            return 0;
        }

这里我们从LinearLayoutManager的垂直滚动分析起,进入scrollBy方法,

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        //滚动发生时,是需要回收View的
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        //手指向上滑动时dy>0
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        //更新LayoutState
        updateLayoutState(layoutDirection, absDy, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        //layout view 结束,所有 View 整体平移;这里需要注意的是 LLM 并非是从头到尾一个个 layout view,而是先根据偏移把需要回收的 view 回收掉,会显示的view显示出来,最后进行整体的平移。想一想这样效率确实要高
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

写了部分注释,具体分析一下 updateLayoutState 方法
分析这个方法前,先看下LayoutState这个类,了解一下我们需要关注的几个重要参数的含义

mRecycle 表示是否需要回收View,滑动情况下这个值是true,但是在有item 添加删除的情况是false,因为锚点什么的得靠view来确定,不能回收

mOffset layout View时候的起始坐标(垂直方向的LinearLayoutManager 表示y值),e.g.比如发生滑动后,下一个item需要显示出来,那么mOffset的值就等于最后一个可见item的bottom值(不考虑margin,向上滑动)

mAvailable 表示可用距离,在layout View的时候用到

mCurrentPosition 表示获取View的起始索引,在layout View的时候循环取View的时候用到

mItemDirection 获取item 数据的方向,是从前到后(值为1),还是从后往前(值为-1),本篇分析的是正序情况

mExtra 在LayoutManager支持predictive动画的时候这个值很有用,具体的需要了解RV的动画机制才明白这个值怎么回事,简单的说就是即使一个item此时(当他即将进入RV可见范围时)对用户不可见,但是还是得把他layout出来,虽然已经超出了RV的边界用户看不到,这样做的目的是为了更好的动画体验(因为需要两次layout,一次pre-layout 一次post-layout来确定动画的起始和终止位置,不然就只能做最简单的fadeIn fadeOut。只有当item add remove发生时才有值,一般为0

再回过来看updateLayoutState方法(并不喜欢贴太长串的代码。。。)

     private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        // If parent provides a hint, don't measure unlimited.
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mExtra = getExtraLayoutSpace(state);
        mLayoutState.mLayoutDirection = layoutDirection;
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            mLayoutState.mExtra += mOrientationHelper.getEndPadding();
            // get the first child in the direction we are going
            final View child = getChildClosestToEnd();
            // the direction in which we are traversing children
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // calculate how much we can scroll without adding new children (independent of layout)
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
            final View child = getChildClosestToStart();
            mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding();
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
                    + mOrientationHelper.getStartAfterPadding();
        }
        mLayoutState.mAvailable = requiredSpace;
        if (canUseExistingSpace) {
            mLayoutState.mAvailable -= scrollingOffset;
        }
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

前面几行就是简单的赋值更新状态,然后是根据 layoutDirection 的方向进行其他参数的计算,我们这里是分析的手指上滑,对应的 layoutDireciton 是 LAYOUT_END ,我们进入layoutDirection == LayoutState.LAYOUT_END 成立的情况下去看:

1.通过 getChildClosestToEnd 方法拿到RV最接近 End 的 child (如果是水平布局,那么end就是RV的right,垂直布局end就是RV的bottom)

2.根据 mShouldReverseLayout 变量给 mItemDirection 赋值,我们一般都不使用逆序布局,所以 mItemDirection 的值是 ITEM_DIRECTION_HEAD ,也就是说待会在fill方法内部取 child 来放的时候是正序,也就是从前往后依次取,反之亦然

3.根据刚才取到的 child 获取到其在 adapter 中的位置,加上mItemDirection后赋值给mCurrentPosition,这个好理解,mCurrentPosition表示的就是取child的开始索引,LayoutState里面有个next方法就是这么做的,可以看下代码

        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

4.给mOffset赋值,也就是下图最后一个item的bottom值,是后续依次放child的起始坐标

5.至于scrollingOffset就是上面这张图里面最后一个 item 底部距离 RV底部的距离,官方也有注释—-“不需要添加新的 children 的情况下滚动的最大距离”—–也就是说最后一个 item 刚好完全滚进来,但是又不会有新的 item 滚进来的意思

6最后给 mAvailable 赋值为 requiredSpace,也就是此次滚动的距离,然后判断canUseExistingSpace 为 true 就减去刚才的 scrollingOffset;这里为什么要减去这个scrollingOffset呢,其实就是把这个零头减掉方便计算而已。这里可以详细解释一下为什么方便计算,比如 滑动的距离不够把最底部的一个 item 滑进来,那么是不是根本不涉及到 添加新 view ,也不用回收 view,这样就是高效的。具体表现在代码里就是 mAvailable 是负值, fill 方法 里面的 那个 for 循环根本不会执行。所以最后一句 mScrollingOffset 又把 scrollingOffset 保存起来了。最后所有 child 都放置好之后,返回消耗的滑动距离时候,在scrollBy方法那里
最后又把这个值加上去了,就这句:

     final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);

好了,这个方法看完了,进去fill方法,没有贴全部的代码,还是一块一块看紧凑点

        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }

这句确实不太明白什么意思也不敢说我现在看明白了。外层这个if是为了避开首次初始化的情况,只有正常滑动的情况时候才会进来,但是滑动情况下 layoutState.mAvailable < 0 这个条件只有在滑动距离过小不足以把最后一个item的底部完全滑进来的情况才满足,不过看官方的注释好像是有bug,可能就做了一个防御性的if判断,防止特殊情况发生把,就假设这个条件满足了,不做if里面的处理,好像也不会发生什么问题把,不过回头想一下mScrollingOffset这个字段的意思是“在不需要添加新的View时候能滑动的最大距离”,按照这么理解,当mAvailable<0时候,说明滑动距离太小,没法把item底部全滑进来,最多也就只能滑动此次滑动的距离,所以这么处理之后mSrollingOffset就是此次滑动距离;所以这个TODO注释看的挺烦的,还以为是bug,让人很纠结是个什么bug-_-,最后recycleByLayoutState方法回收了一下此次滚动发生之后会越界不见的View

       private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }

根据滑动方向选择是从后往前回收还是从前往后回收,我们考虑手指上滑,所以可能会有头部的View出界被滑出去,所以是调用的recycleViewsFromStart方法

  private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
        if (dt < 0) {
            if (DEBUG) {
                Log.d(TAG, "Called recycle from start with a negative value. This might happen"
                        + " during layout changes but may be sign of a bug");
            }
            return;
        }
        // ignore padding, ViewGroup may not clip children.
        final int limit = dt;
        final int childCount = getChildCount();
        if (mShouldReverseLayout) {
            for (int i = childCount - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, childCount - 1, i);
                    return;
                }
            }
        } else {
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }

看代码的第一句,如果 dt<0 就直接返回结束了,这也解释了为什么前面的纠结为什么当mAvailable < 0时候让 mScrollingOffset 加上 mAvailable,就是为了让这里传入的dt是正值,也就是实际发生的滑动距离。由于不考虑逆序布局,直接看第二个 for循环,其实这个循环要表达的意思是从头到尾遍历所有View直到找到一个滑动之后底部还没出界的View,那么在这个View之前的View全部要被回收掉。所谓回收掉就是把View节点从ViewHierarchy上删除掉了,但是被缓存起来了供重新绑定和重用。

继续往fill方法下面看,进入while循环

      while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }

只要还有可用空间就依次取 View 并添加layout出来,之后更新mOffset和mAvailable,当然如果某个view是有焦点的,那么直接结束

看LayoutChunk方法,顾名思义,就是layout小块的意思,就是把单个的itemView放置到合适的位置,并且传入了一个LayoutResult用于记录放置Item后的信息,就几个字段:
mConsumed 消耗的距离
mFinished 是否结束layout
mIgnoreConsumed 是否忽略此次消耗的距离,滑动情况下这个值一直都是false
mFocusable 当前item是否有焦点

layoutChunk方法里面的逻辑,也没什么,就是测量,然后计算left top right bottom值。有一段逻辑比较重要,判断了mScrapList 是否为null,如果是null就调用了addDisappearingView方法,反之调用了addView;addView方法不用说就是简单的添加了View,但是addDisappearingView就是告诉RV,添加的这个View是马上就要移出屏幕的,注意是不可见了并不代表就是item被移除了也有可能是在屏幕之外。好了,我们再回头想象为什么是判断mScrapList为null就调用addDisappearingView。具体原因是mScapList其实绝大部分情况都是null,只有发生layout时候才不为空,而这个时候都是发生了item的增删改操作,导致有些View可能会超出RV的边界,也就是变成所谓的“hidden view”,不要被这个方法名迷惑,只是尝试加入hidden view,方法内部实际还是会根据flag判断之后决定是否需要hide 这个view。反过来mScrapList为null的时候就是对应滑动情况。

layoutChunk 方法内容不多,另外还需要注意的是这句:

    if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }

意思很简单,就是Item被删除了或者变化了,就忽略消耗,也就是不计入消耗。最开始我也觉得奇怪,后来知道动画之后明白这么做是有意义的,虽然这个Item被删除了,但是你不能立马就给不显示了还是添加进来,毕竟还有动画在这个Item要执行,所以就得等到这个Item的动画完了才删除。那么不计入消耗的好处就是,会多layout一个Item出来,就是在底部,屏幕外面,虽然不可见,如图:

item 2已经被移除了,并且item5会被加进来,但是在屏幕外我们看不到,等到item2动画结束item5就会滑进来。当然这个if判断在滑动情况下是不会进来的。

最后,所有的view添加完之后,其实 view 并没有在正确的位置,所以整体又进行平移,至此整个滑动流程都结束。

上面只是滑动的流程。实际上比较复杂的还是 出现 item 增删的时候,多次布局,并且配合 动画的时候。所以 RecyclerView 的架构设计真的很厉害,把很多东西组合在一起,还很优雅。

the end

Hexo+github搭建个人博客

Posted on 2017-09-05

很早之前就有人用hexo和github提供的page服务做个人博客了,不过了解一下就没有怎么关注了,最近有时间,看了一下官方文档,花了两个多小时,搭建了一个简单的个人博客,除了最开始搭建配置繁琐一点,后面写完一篇文章一个命令就发布,体验非常棒!

新建主页仓库

登录自己的github账户,新建一个仓库,比如我的用户名是HirayClay,那么我就新建一个名为
HirayClay.github.io的仓库

hexo环境搭建

首先要安装必要的软件,Node.js和Git,安装完成之后安装hexo

   $ npm install -g hexo-cli

我安装的是 3.3.3版本
hexo安装好之后就可以用hexo命令创建一个站点了

    $ hexo init 
    $ cd 
    $ npm install

看一下创建的目录结构

    .
    ├── _config.yml
    ├── package.json
    ├── scaffolds
    ├── source
    |   └── _posts
    └── themes

其中_config.yml是配置文件,一些全局的重要配置都在这里面;package.json文件中声明了版本信息和依赖信息,scaffolds,即脚手架的意思,我们创建post的时候就是用的这个文件下的模板,里面默认有三种模板:draft、page、post,当然你也可以创建自己的模板;source目录下有个子目录_posts,顾名思义就是放我们文章的地方;最后themes就是存放主题的地方,可以下载三方的主题放在里面

我们需要重点关注一下_config.yml文件里面几个地方

    title: Blog
    subtitle: hirayclay's blog
    description:
    author: hirayclay
    language: zh
    timezone: Asia/Shanghai

title:站点的标题,subtitle:站点子标题,description:站点描述 language:站点语言,这里配置的是中文,其他语言的参考这里>>,timezone即时区,这里用的中国北京时区,其他时区参考>>

要注意一点的是,这里所有的配置“:”后面都需要有一个空格,不然最后解析生成时候会失败,这是YAML的语法

    new_post_name: :title
    default_layout: post

new_post_name 即生成的post的名称,有一下几个配置:
:title
:year
:month
:i_month
:day
:i_day
我这里直接用title命名生成的post文件

比如执行一下命令

    hexo new post "MyNewPost" 

就会在source/_posts目录下生成 MyNewPost.md文件

再看下语法高亮配置,比较简单,常用到的是否禁用和代码行数

    highlight:
    enable: false
    line_number: true
    auto_detect: false
    tab_replace:

部署配置,第一步新建仓库的作用到了

  deploy:
  type: git
  repo: git@github.com:HirayClay/HirayClay.github.io.git
  branch: master

因为我们用的git提交到远程仓库的,所以type = git,以及仓库地址和分支名,这里的repo就是之前新建的仓库地址

配置说完了,基本可以开写了

首先 用 hexo new post <your_post_name> 创建一篇博客,然后source/_posts目录找到对应的博客打开编辑即可,可以给博客加tag,比如本博客的tag

   ---
    title: Hexo+github搭建个人博客
    date: 2017-09-05 14:04:10
    tags:
        - hexo
        - 博客
    ---

至此基本的都配置完成了,用以下命令生成静态资源

    hexo generate

也可以写成

    hexo g

然后可以本地起一个服务进行预览

    hexo server

浏览器输入http://localhost:4000 进行查看

用以下命令发布到git远程仓库

    hexo deploy

应该会弹出一个窗口让你你输入ssh-key的密码

最后如果觉得默认主题不合适,可以去下载其他主题到 themes目录下,可以随意命名该目录下的主题文件夹,但是最后在_config.yml文件中配置主题时候一定要用文件夹的名字

    theme: theme_folder_name

The End

利用模板写Span

Posted on 2017-09-05

在之前的项目中,PM特别喜欢把一些文字做颜色或者大小上的区分,所以经常会用到Span,没有什么好的封装想法,只能老老实实的用原始的api,显得非常的笨,但是又没有什么办法,没想到什么好的封装策略,只是觉得这样写真的好难看啊。但是一般需要做特殊处理的文字其实都是后台返回的某些字段,是有特别含义的,比如“距离审核还有6天结束”中的‘6’其实就是后台会单独返回给你的。我们App这边拼接好整句话然后显示出来。当时在做这样的项目的时候也找过类似的开源库,但是觉得总觉得哪里不对,也懒得用,还是用的原始的套路,先数一数‘6’在字符串中的起始结束下标,然后设置Span。直到最近为了深入了解gradle,去看了下groovy,看到“Template engines”的时候突然想起之前的Span,于是有了一个大胆的想法。

关于 Groovy的Template

Groovy可以动态生成字符串,比如模板是这样的’${name} is ${age} years old! ‘
绑定关系是这样的:[name:”Alice”,age:”18”],那么生成的文字就是”Alice is 18 years old!”。你可能要问了,这和你说的Span有什么关系???当然有,前面我们说了,我们的需要设置Span的文字其实都是有含义的,我们用原始的api那样数出下标然后设置Span非常的无脑,根本没有体现出这个字段的含义,但是现在如果我们用groovy的方式,定义自己的模板那么”距离审核还有6天结束”的模板是不是就是”距离审核还有${day}天结束”,这样表达起来是不是更有内涵些,然后你又要问我,确实有内涵了,但这和Span又有什么关系呢??好吧,也没什么关系,就是要有内涵一点,所以借用Groovy的思想重新封装对Span的处理。你可能还要问,不是已经有类似的库了吗,干嘛还要封装一个,比如Spanny。那好,我们看看Spanny怎么做的,

    Spanny spanny = new Spanny("距离审核还有")
                .append("6", new ForegroundColorSpan(Color.RED))
                .append("天结束!");
    textView.setText(spanny);

对比我们定义好一个模板 “距离审核还有${day}天结束”,比较一下就看出不同了。Spanny的做法是希望需要什么样的Span就自己拼一个,虽然配合链式调用挺舒服的,其实给人的感觉就是很分离,并不像一句完整的句子那么看起来实在,个人觉得还是Groovy这样的模板很合适,毕竟当需要处理Span的时候,结构都是死的,所以用模板定义好结构是没有问题的,特别是当要处理的文字比较多的时候,这样拼接我觉得不太好,用定义的模板一眼看过去就非常的清晰明了。

需要解决的问题

虽然引入了template的思想来动态生成字符串,同时又需要对key替换后的文字做对应的处理,那么要解决的问题有以下三个:
1 如何解析模板字符串
2 如何替换key并生成结果字符串
3 如何解决以上两个问题

关于第一个问题,看了下groovy解析模板的代码,自己做了一下修改差不多就是抄过来的,只是加入了一些额外的逻辑)
最后解析模板的代码是这样的:

List<MarkInfo> parseAndMark(Reader reader, Map<String, String> binding) {
        if (!reader.markSupported())
            reader = new BufferedReader(reader);
        List<MarkInfo> markers = new ArrayList<>();
        MarkInfo mark;
        StringWriter writer = new StringWriter(50);
        while (true) {
            int c;
            try {
                if ((c = reader.read()) != -1) {
                    if (c == '$') {
                        reader.mark(1);
                        c = reader.read();
                        if (c == '{') {
                            String key = findKey(reader);
                            //only true for text
                            if (!key.isEmpty()) {
                                String value = binding.get(key);
                                //for text
                                if (value != null) {
                                    int start = writer.getBuffer().length();
                                    int end = start + value.length();
                                    markers.add(new MarkInfo(key, value, start, end));
                                    writer.write(value);
                                } else {//for image

                                    int start = writer.getBuffer().length();
                                    int end = start + key.length();
                                    markers.add(new MarkInfo(key, value, start, end));
                                    writer.write(key);
                                }
                            } else {
                                writer.write("${");
                                //key not found
                                writer.write(key);
                            }

                        } else {
                            writer.write('$');
                            reader.reset();
                        }
                    } else {
                        writer.write(c);
                    }
                } else break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return markers;
    }


    String findKey(Reader reader) {
        StringWriter stringBuilder = new StringWriter(10);
        int c;
        try {
            while ((c = reader.read()) != -1) {
                if (c == '}')
                    break;
                else stringBuilder.write(c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }

这个方法的作用就是记录模板中所有key的起始结束位置。比如原始模板是:”${name} is ${age} years old! “数据映射是[name:”Alice”,age:”18”],解析之后就变成”Alice is 18 years old! “。并且’Alice’ ‘18’两个数据在字符串中的位置被记录在了MarkrInfo中

我们来简单分析一下代码,一个简单的while循环,每次读取一个字符,每当读到’$’字符时认为可能是key要出现了,所以先在此处标记一下紧接着读取下一个字符,如果读到下一个字符是’{‘则认为key出现了,调用findKey方法读取’{‘和’}’之间的key值,如果为空则认为没有key,仅仅是读到了一个普通的”${}”,并且写入writer保存起来,如果key不为空认为读取到有效的key,记录key对应的value在字符串中的位置等信息,并且将value写入writer保存起来;如果’$’后面读到的不是’}’则认为只是读到了一个单独的’$’字符,虚惊一场,写入writer保存起来,并且把reader 重置,回到刚才标记的地方,也就是’$’的位置;如果读取的是普通的字符,直接写入writer.另外要说的就是ImageSpan的处理,由于有些字符最后是要替换成图片的,所以在binding中是没有其对应value的,所以当读取的key在binding中如果没有value,就认为这个key是要被替换成图片的,所以直接用key代替value,直接把key写入writer保存起来。

解析这一步完成以后我们其实得到了一个List,记录了key被替换成value后的value在结果字符串中的位置信息,以及原始的key等信息。有了这些重要信息,就可以根据下标施加对应的Span了,以及一些点击事件的监听了。

施加Span的时候需要考虑文字和ImageSpan的差别,绝大多数时候是对文字的处理,不过有一种是把文字替换成图像,所以这个key在bingding中对应的就是个null,所以在施加span的时候都会判断一下是否为空,为空则说明是个ImageSpan,就不会做除了ImageSpan之外的任何处理

12

hirayclay

16 posts
29 tags
© 2020 hirayclay
Powered by Hexo
|
Theme — NexT.Muse v5.1.4