基础界面

我们要开发的App界面如下

clipboard.png

  1. 有一个title
  2. 一个列表
  3. 右下角一个按钮
  1. title 可以用系统自带的ActionBar实现(Lollipop以上为Toolbar)。
  2. 下面的列表可以用ListView或者android-support-compact-v7提供的新的RecyclerView。展示一个列表。
  3. 按钮可以使用普通的Button,我这里为了符合Material Design规范,使用了FloatingActionButton,没什么不同,只是展示出来的样式不一样而已。

整个界面就这么简单。

我们可以看到Toolbar以下的部分是列表和按钮,它们的排列不是属于线性排列,所以我们就想到用RelativeLayout布局 —— 列表占满空间,而按钮存在容器的右下角。
我们的布局文件就如下:

<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"
    tools:context=".MainActivity">

    <org.lifefortheorc.tudounotepad.widget.EmptyRecyclerView
        android:id="@+id/recycler_note"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextView
        android:id="@+id/tv_empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/empty"
        android:gravity="center"
        android:layout_centerInParent="true" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_marginEnd="@dimen/activity_horizontal_margin"
        android:layout_marginBottom="@dimen/activity_vertical_margin"
        android:src="@drawable/ic_add" />

</RelativeLayout>

这其中的tv_empty是我们用来做一个列表空内容的提醒的,触发的结果如下
当列表不为空时隐藏,当列表为空时就显示
clipboard.png

关于列表

不管是ListView还是RecylerView,如果你熟悉iOS开发的话,会理解item重用的机制——Android为了省资源,只会创建一屏幕的item,如果你的列表项目数目大于一个屏幕,当你滚下列表时,系统会把顶上的item回收,在回调函数里提供给你重新使用,然后重新在底部显示出来,这样的好处是你创建的item不论你实际数据的大小,它至多只创建一个屏幕多的数量,能节省资源。

列表、子视图和数据之间的交互使用了适配器模式,在Adapter中把你的数据渲染到item view上,然后应用给父视图。
关于ListView的介绍,请看传送门:http://developer.android.com/guide/topics/ui/layout/listview.html

如果你看懂了ListView,那么RecylerView只是对其的一种优化,使用了更多的自定义的属性和更方便自由的布局管理系统。

我这里使用了RecylerView,我们来看下适配器的源码

public class NoteAdapter extends RecyclerView.Adapter<NoteAdapter.ViewHolder> {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");

    private Context mContext;
    private List<NoteModel> noteList;

    public NoteAdapter(Context ctx) {
        this.noteList = new ArrayList<>();
        this.mContext = ctx;
    }

    public void setNoteList(List<NoteModel> noteList) {
        //设置数据
        this.noteList.clear();
        this.noteList.addAll(noteList);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        View view = layoutInflater.inflate(R.layout.item_note, viewGroup, false);
        //返回默认界面布局
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        NoteModel note = noteList.get(position);

        String content = note.getContent();
        long time = note.getTime();

        //根据数据渲染界面
        viewHolder.mTextViewContent.setText(content);
        viewHolder.mTextViewTitle.setText(dateFormat.format(time));
    }

    public void remove(int position) {
        NoteModel note = this.noteList.get(position);
        note.delete();
        this.noteList.remove(position);
    }

    @Override
    public int getItemCount() {
        return noteList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        @Bind(R.id.tv_title)
        public TextView mTextViewTitle;
        @Bind(R.id.tv_content)
        public TextView mTextViewContent;

        public ViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            int position = this.getAdapterPosition();
            NoteModel note = noteList.get(position);


            long id = note.getId();
            Intent intent = new Intent(mContext, EditActivity.class);
            intent.putExtra("id", id);
            ActivityOptions options =  ActivityOptions.makeSceneTransitionAnimation((Activity) mContext,
                    v, "content");
            mContext.startActivity(intent,
                    options.toBundle());
        }
    }

}

靠这一套AdapterRecyclerView就能一项一项渲染出我们要的列表了。

关于数据

渲染界面少不了数据,我们创建数据的入口在右下角,点"+"号就可以跳转到新建的页面
代码如下:

Intent intent = new Intent(this, EditActivity.class);
startActivity(intent);

两行即可搞定。

我们跳转到新的界面

clipboard.png
在这里用户可以输入想要的内容,点右上角的保存即可保存。保存到内存很简单,我们如何把数据持久化呢?
Android系统提供了SQLite来给我们持久化数据。SQLite是一种轻量级的文件关系型数据库,可以满足我们的需求,我们使用SQLite存储用户输入的数据,然后保存到本地,就是这样。

关于Android中,SQLite的部分,可以看这个传送门:http://developer.android.com/training/basics/data-storage/databases.html

在这里,我们为了方便直接使用ActiveAndroid的ORM方案(http://www.activeandroid.com/),可以
Model直接持久化到数据库,然后从数据库中读取数据。
我们的模型如下:

@Column(name = "content")
private String content;

@Column(name = "time")
private long time;

public String getContent() {
    return content;
}

public void setContent(String content) {
    this.content = content;
}

public long getTime() {
    return time;
}

public void setTime(long time) {
    this.time = time;
}

public static List<NoteModel> queryNoteList() {
    return new Select().from(NoteModel.class).execute();
}

public static NoteModel queryById(long id) {
    return new Select().from(NoteModel.class)
                       .where("Id = ?", id).executeSingle();
}

我们记录用户存入的内容,存入的事件,然后定义两个方法——列出所有存着的列表和根据id查询其中一个对象,我们使用这两个方法就能获取到列表和详情两个数据。

列表数据提供给首页,详情数据提供给编辑页面。这样就满足了一个记事本App的基础需求——增加与修改。

列表页:

clipboard.png

详情页:

clipboard.png

如果对于写App入门还有什么问题,欢迎留言以及在Github上follow我

本文提到的项目源码地址:https://github.com/geminiwen/tudounotepad
欢迎留言Github或者@geminiwen

注: 这是为想入门Android的新手准备的一篇文章

想学习写Android App么? 其实很简单,哦,再简单之前,也要先学java

.......

好了,你入门java了,那就可以来看看用一天时间写一个App是多么容易。
我们来写个记事本吧。

OK,先下载Android StudioAndroid Studio是Google官方推荐的IDE,能快速的开发Android App.

下载地址 http://developer.android.com/sdk/index.html

第一步创建工程

我们新建一个项目,点Start a new Android Studio project 即可。

clipboard.png

第二步 输入一些项目的属性。

Application name就是你应用的名称,Company Domain是你公司的域名,如果你是个人开发者,就写你自己的域名即可。
Package name是包名,理论上要求在地球范围内唯一,它标识了你这个App。

clipboard.png

第三步 输入支持的最低版本的SDK

这里我们只创建手机和平板应用。Minimum SDK是什么呢?比如你如果选择Android 5.0,那么5.0以下的设备就不能安装你的App,兼容性越差。 我们知道Android每次更新版本都会增加许多新特性,也就意味着Android SDK版本越高,特性越多,兼容性越差。 如果你的App受众很大,建议选择低版本的SDK,并采用一些替代方案来实现你要的功能,我们这里如果只是为了学习SDK,可以直接使用Lollipop,否则我推荐IceScreamSandwich
clipboard.png

第四步 创建一个页面

这里要介绍下,Android中的UI都是通过Activity来呈现,一个Activity可以理解成你看到的“窗口”,你从一个列表跳到一个详情,可以简单的认为从一个Activity跳到另外一个Activity。这里Android Studio会通过向导模式帮你创建一个入口Activity,我们可以选择不创建,或者第一个空白ActivityBlank Activity
clipboard.png

第五步 设置Activity的属性

这一步可以设置Activity的java类名,Activity的布局,Activity显示的标题以及菜单的资源。
Activity通过布局文件(layout)来决定显示怎么样的界面,Title是Activity顶部显示的一行文本,menu是Android手机点menu键或者点右上角的...显示的菜单。

clipboard.png

完成创建

点击finish,一个工程就创建完了,这就是最基本的创建工程的步骤。

本文提到的项目源码地址:https://github.com/geminiwen/tudounotepad
欢迎留言Github或者@geminiwen

之前看到简书Android客户端使用的编辑器,甚是喜欢,它的优雅以及高性能的特点让我爱不释手,很想自己也去做一个。
此前实现过一个在Android上的Markdown编辑器
但是界面以及所见即所得的效果非常不好看,所以一直耿耿于怀。

然后冒昧看了下简书的布局系统,看见了几个奇怪的类,包括类似XWalkContentView,于是Google了下,就查到了CrossWalk这个hybrid框架了。第一眼并不觉得它有啥不一样,以为是一个Cordova的轮子。后来细看,发现是自个儿编辑了整个Chrominum,屌屌屌!

运行个demo,wrapper了一个http://sf.gg 发现体验真的是不错啊,webview性能到这个水平内心都宽慰了,但是为何安装速度那么慢呢?一看apk大小,足足有40M+,感觉天都要塌了。SegmentFault for Android 客户端才3.03M,我要是包上这玩意,估计就没多少人下了吧。。。然后又看看简书,整个apk大小才8M,在启动编辑器的时候,提示需要下载编辑器,下载了一会,然后再打开。顿时就明白了,看来它的库是从外部载入的,记得以前看到过从外部加载动态链接库想想很是简单,于是入坑了。

好嘛,我把so文件先不放进apk中,让apk装好之后,放入/data/data/<app>/lib目录下,启动app,直接crash。
看日志入下:

DavlikDexClassLoader Unsatisfied Link library['/xxxx/xxx.apk', '/vendor/lib', '/system/lib']

一看这个路径,泪奔了,原来library path只有三个路径下去检查,算了,我们不是有System.loadSystem.loadLibrary函数么,直接调用呗,于是我就先暂时把绝对路径给写了下来,直接调用System.load函数。

再次启动,发现CrossWalkShared Library should use SharedXWalkView。但是使用SharedXWalkView有许多的限制,比如需要安装一个CrossWalk Runtime的apk,奇怪了,它怎么知道我是用Shared Library的呢?而且简书也没有说要安装apk啊。

于是我继续研究,开始看CrossWalk的源码,找到ReflectionHelper这个类里面有一行代码shouldUseLibrary(),它会去调用System.loadLibrary()如果没有报异常,则返回false,否则返回true

我们知道System.loadLibrary这个函数,会去java.library.path这个环境变量的路径下面寻找库,而Android是不允许我们更改这个环境变量的值的,就导致CrossWalk认为并没有加载它的runtime而去开启Shared模式。

OK,知道怎么解决就方便了,首先,我们要把so文件放入到/data/data/<app>/下的任意路径,因为我们的apk有这个权限在这里放东西,然后使用System.load加载这个so库,最后使用反射的方式欺骗CrossWalk框架,告诉它我们的类库已经加载完毕。

我们仔细研究下它的源码,发现有几个标志位需要更改,具体代码如下:

System.load(libPath);
try {
    LibraryLoader loader = LibraryLoader.get(1);
    Class c = Class.forName("org.xwalk.core.internal.XWalkViewDelegate");
    Field field = c.getDeclaredField("sLibraryLoaded");
    field.setAccessible(true);
    field.setBoolean(null, true);
    field.setAccessible(false);

    field = LibraryLoader.class.getDeclaredField("mLoaded");
    field.setAccessible(true);
    field.setBoolean(loader, true);
    field.setAccessible(false);
    PathUtils.setPrivateDataDirectorySuffix("xwalkcore");

} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (ProcessInitException e) {
    e.printStackTrace();
}

只要把以上的类中的标志位更改掉,那么CrossWalk就认为库已经加载成功了。

Android M Preview发布后,我们获得了一个新的support library —— Android Design Support Library 用来实现Google的Material Design 提供了一系列符合设计标准的控件。

其中有众多的控件,其中最复杂,功能最强大的就是CoordinatorLayout,顾名思义,它是用来组织它的子views之间协作的一个父view。CoordinatorLayout默认情况下可理解是一个FrameLayout,它的布局方式默认是一层一层叠上去。
那么,CoordinatorLayout的神奇之处就在于Behavior对象了。
看下CoordinatorLayout.Behavior对象的 Overview

Interaction behavior plugin for child views of CoordinatorLayout.

A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.

可知Behavior对象是用来给CoordinatorLayout的子view们进行交互用的。
Behavior接口拥有很多个方法,我们拿AppBarLayout为例。AppBarLayout中有两个Behavior,一个是拿来给它自己用的,另一个是拿来给它的兄弟结点用的,我们重点关注下AppBarLayout.ScrollingViewBehavior这个类。

我们看下这个类中的以下方法

0. dependency

 public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency)     {
    return dependency instanceof AppBarLayout;
}

这个方法告诉CoordinatorLayout,这个view是依赖AppBarLayout的,后续父亲可以利用这个方法,查找到这个child所有依赖的兄弟结点。

1. measure

public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

这个是CoordinatorLayout在进行measure的过程中,利用Behavior对象对子view进行大小测量的一个方法。
在这个方法内,我们可以通过parent.getDependencies(child);这个方法,获取到这个child依赖的view,然后通过获取这个child依赖的view的大小来决定自身的大小。

2. layout

 public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection)

这个方法是用来子view用来布局自身使用,如果依赖其他view,那么系统会首先调用

public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) 

这个方法,可以在这个回调中记录dependency的一些位置信息,在onLayoutChild中利用保存下来的信息进行计算,然后得到自身的具体位置。

3. nested scroll

 public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes)
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) 
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) 
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target) 

这几个方法是不是特别熟悉?我在Android嵌套滑动机制(NestedScrolling) 介绍过,这几个方法刚好是NestedScrollingParent的方法,也就是对CoodinatorLayout进行的一个代理(Proxy),即CoordinatorLayout自己不对这些消息进行处理,而是传递给子view的Behavior,进行处理。利用这样的方法,实现了view和view之间的交互和视觉的协同(布局、滑动)。

总结

可以看到CoodinatorLayout给我们实现了一个可以被子view代理实现方法的一个布局。这和传统的ViewGroup不同,子view从此知道了彼此之间的存在,一个子view的变化可以通知到另一个子view。CoordinatorLayout所做的事情就是当成一个通信的桥梁,连接不同的view。使用Behavior对象进行通信。

我们具体的实现可以参照 Android官方文档告诉我们的每一个方法的作用 进行重写,实现自己想要的各种复杂的功能。
https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html
有了这么一套机制,想实现组件之间的交互,就更加方便快捷啦~

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性

NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView

图片描述

效果如上图【别嫌弃我】

在这之前,我们知道AndroidTouch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEventonInterceptTouchEventonTouchEvent

这种分发机制有一个漏洞:

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下

NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

对,简单的话你就这么实现就好了。

这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法

    /**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link NestedScrollingChild} interface method with the same signature to implement
     * the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link NestedScrollingChild#startNestedScroll(int)}.
     * @return true if a cooperating parent view was found and nested scrolling started successfully
     */
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:

一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent

二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。

三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。

四、stopNestedScroll

结束整个流程。

整个对应流程是这样

子view父view
startNestedScrollonStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScrollonNestedPreScroll
dispatchNestedScrollonNestedScroll
stopNestedScrollonStopNestedScroll

一般是子view发起调用,父view接受回调。

我们最需要关注的是dispatchNestedPreScroll中的consumed参数。

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。


对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。