Android-底部Tab菜单栏

前言

Android开发中使用底部菜单栏的频次非常高,主要的实现手段有以下:

  • TabWidget
  • 隐藏TabWidget,使用RadioGroup和RadioButton
  • FragmentTabHost
  • 5.0以后的TabLayout
  • 最近推出的 Bottom navigation

今天带大家来探索下如何用Fragment+FragmentTabHost++ViewPager
实现底部菜单栏

总体设计思路

  • Fragment:存放不同选项的页面内容
  • FragmentTabHost:点击切换选项卡
  • ViewPager:实现页面的左右滑动效果

概念介绍

FragmentTabHost

用于实现点击选项进行切换选项卡的自定义效果

使用FragmentTabHost,就是先用TabHost“装着”Fragment,然后放进MainActivity里面

ViewPager

  • 定义
    ViewPager是android扩展包v4包中的类

android.support.v4.view.ViewPager

  • 作用
    左右切换当前的view,实现滑动切换的效果。

注:

1.ViewPager类直接继承了ViewGroup类,和LinearLayout等布局一样,都是一个容器,需要在里面添加我们想要显示的内容。

2.ViewPager类需要PagerAdapter适配器类提供数据,与ListView类似

3.Google官方建议ViewPager配合Fragment使用

Fragment

  • 定义
    Fragment是activity的界面中的一部分或一种行为

1.把Fragment认为模块化的一段activity

2.它具有自己的生命周期,接收它自己的事件,并可以在activity运行时被添加或删除

3.Fragment不能独立存在,它必须嵌入到activity中,而且Fragment的生命周期直接受所在的activity的影响。例如:当activity暂停时,它拥有的所有的Fragment们都暂停了,当activity销毁时,它拥有的所有Fragment们都被销毁。

  • 作用
    主要是为了支持更动态、更灵活的界面设计(从3.0开始引入)

实现步骤

  1. 在主xml布局里面定义一个FragmentTabHost控件
  2. 定义底部菜单栏布局
  3. 定义每个Fragment布局
  4. 定义每个Fragment的Java类
  5. 定义适配器以关联页卡和ViewPage
  6. 定义MainActivity(具体实现请看注释)

工程文件目录

20171105150989723658930.png
20171105150989723658930.png

具体实现实例

步骤1:在主xml布局里面定义一个FragmentTabHost控件

主xml布局:Main_tab_layout.xml

<?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:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

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

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" /><!--装4个Fragment-->

    <FrameLayout
        android:visibility="gone"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <!--定义FragmentTabHost控件-->
    <android.support.v4.app.FragmentTabHost
        android:id="@android:id/tabhost"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/black" ><!--装4个Fragment-->

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0" /><!--装Tab的内容-->
    </android.support.v4.app.FragmentTabHost>
</RelativeLayout>

步骤2:定义底部菜单栏布局

tab_content.xml

一般是图片在上,文字在下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    
android:layout_width="match_parent"    
android:layout_height="match_parent"    
android:gravity="center"   
android:orientation="vertical"    
android:background="#ffffff">    
<ImageView        
android:id="@+id/tab_imageview"        
android:layout_width="wrap_content"        
android:layout_height="wrap_content"        
/>    
<TextView        
android:id="@+id/tab_textview"        
android:layout_width="wrap_content"        
android:layout_height="wrap_content"        
android:text=""        
android:textColor="@drawable/selector_text_background" />
</LinearLayout>

步骤3:定义Fragment布局

fragment_item1.xml&fragment_item2.xml

这里使用两个选项,由于fragment_item1.xml与fragment_item2.xml相同,这里只贴出一个

fragment_item1.xml

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

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="fragment1"
        android:textSize="20sp"/>

</LinearLayout>

步骤4: 定义每个Fragment的Java类

1.这里使用两个选项:Fragment1.java&fragmen2.java

2.由于Fragment1.java&fragmen2.java相同,这里只贴出一个

Fragment1.java

package com.example.carson_ho.tab_menu_demo;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Carson_Ho on 16/5/23.
 */
public class Fragment1 extends Fragment

    {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_item1, null);
        return view;
    }
}

步骤5: 定义适配器关联页卡和ViewPage

MyFragmentAdapter.java

package com.example.carson_ho.tab_menu_demo;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

import java.util.List;

/**
 * Created by Carson_Ho on 16/5/23.
 */
public class MyFragmentAdapter {extends FragmentPagerAdapter

    {

        List<Fragment> list;



        public MyFragmentAdapter(FragmentManager fm,List<Fragment> list) {
        super(fm);
        this.list=list;
    }//写构造方法,方便赋值调用
        @Override
        public Fragment getItem(int arg0) {
        return list.get(arg0);
    }//根据Item的位置返回对应位置的Fragment,绑定item和Fragment

        @Override
        public int getCount() {
        return list.size();
    }//设置Item的数量

    }

步骤6: 定义MainActivity

具体实现看注释

MainActivity.java

package com.example.carson_ho.tab_menu_demo;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTabHost;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TabWidget;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends FragmentActivity implements
        ViewPager.OnPageChangeListener, TabHost.OnTabChangeListener {

    private FragmentTabHost mTabHost;
    private LayoutInflater layoutInflater;
    private Class fragmentArray[] = { Fragment1.class, Fragment2.class };
    private int imageViewArray[] = { R.drawable.tab_home_btn, R.drawable.tab_view_btn };
    private String textViewArray[] = { "首页", "分类"};
    private List<Fragment> list = new ArrayList<Fragment>();
    private ViewPager vp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();//初始化控件
        initPage();//初始化页面
    }

    //    控件初始化控件
    private void initView() {
        vp = (ViewPager) findViewById(R.id.pager);

        /*实现OnPageChangeListener接口,目的是监听Tab选项卡的变化,然后通知ViewPager适配器切换界面*/
        /*简单来说,是为了让ViewPager滑动的时候能够带着底部菜单联动*/

        vp.addOnPageChangeListener(this);//设置页面切换时的监听器
        layoutInflater = LayoutInflater.from(this);//加载布局管理器

        /*实例化FragmentTabHost对象并进行绑定*/
        mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost);//绑定tahost
        mTabHost.setup(this, getSupportFragmentManager(), R.id.pager);//绑定viewpager

        /*实现setOnTabChangedListener接口,目的是为监听界面切换),然后实现TabHost里面图片文字的选中状态切换*/
        /*简单来说,是为了当点击下面菜单时,上面的ViewPager能滑动到对应的Fragment*/
        mTabHost.setOnTabChangedListener(this);

        int count = textViewArray.length;

        /*新建Tabspec选项卡并设置Tab菜单栏的内容和绑定对应的Fragment*/
        for (int i = 0; i < count; i++) {
            // 给每个Tab按钮设置标签、图标和文字
            TabHost.TabSpec tabSpec = mTabHost.newTabSpec(textViewArray[i])
                    .setIndicator(getTabItemView(i));
            // 将Tab按钮添加进Tab选项卡中,并绑定Fragment
            mTabHost.addTab(tabSpec, fragmentArray[i], null);
            mTabHost.setTag(i);
            mTabHost.getTabWidget().getChildAt(i)
                    .setBackgroundResource(R.drawable.selector_tab_background);//设置Tab被选中的时候颜色改变
        }
    }

    /*初始化Fragment*/
    private void initPage() {
        Fragment1 fragment1 = new Fragment1();
        Fragment2 fragment2 = new Fragment2();

        list.add(fragment1);
        list.add(fragment2);

        //绑定Fragment适配器
        vp.setAdapter(new MyFragmentAdapter(getSupportFragmentManager(), list));
        mTabHost.getTabWidget().setDividerDrawable(null);
    }

    private View getTabItemView(int i) {
        //将xml布局转换为view对象
        View view = layoutInflater.inflate(R.layout.tab_content, null);
        //利用view对象,找到布局中的组件,并设置内容,然后返回视图
        ImageView mImageView = (ImageView) view
                .findViewById(R.id.tab_imageview);
        TextView mTextView = (TextView) view.findViewById(R.id.tab_textview);
        mImageView.setBackgroundResource(imageViewArray[i]);
        mTextView.setText(textViewArray[i]);
        return view;
    }


    @Override
    public void onPageScrollStateChanged(int arg0) {

    }//arg0 ==1的时候表示正在滑动,arg0==2的时候表示滑动完毕了,arg0==0的时候表示什么都没做,就是停在那。

    @Override
    public void onPageScrolled(int arg0, float arg1, int arg2) {

    }//表示在前一个页面滑动到后一个页面的时候,在前一个页面滑动前调用的方法

    @Override
    public void onPageSelected(int arg0) {//arg0是表示你当前选中的页面位置Postion,这事件是在你页面跳转完毕的时候调用的。
        TabWidget widget = mTabHost.getTabWidget();
        int oldFocusability = widget.getDescendantFocusability();
        widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);//设置View覆盖子类控件而直接获得焦点
        mTabHost.setCurrentTab(arg0);//根据位置Postion设置当前的Tab
        widget.setDescendantFocusability(oldFocusability);//设置取消分割线

    }

    @Override
    public void onTabChanged(String tabId) {//Tab改变的时候调用
        int position = mTabHost.getCurrentTab();
        vp.setCurrentItem(position);//把选中的Tab的位置赋给适配器,让它控制页面切换
    }

}
2017/10/18 posted in  Android

Android-过渡动画学习

概述

Android 4.4.2 (API level 19)引入Transition框架,之后很多APP上都使用该框架做出很酷炫的效果,如 Google Play Newsstand app

20171105150989405862477.gif
20171105150989405862477.gif

还有github上很火的plaid

20171105150989409049352.gif
20171105150989409049352.gif

在app中适当得使用上Transition能带来较好的用户体验,视频中介绍了该框架的基本使用以及其中核心的一些类和方法,只有学会这些基本的API才能在之后的Activity/Fragment过渡定制一些自己想要的效果。

先看官网的一张关系图

2017110515098941297532.png
2017110515098941297532.png

图中有三个核心的类,分别是Scene、Transition和TransitionManager,下面对这个三个核心类展开分析。

Scene

20171105150989413916140.png
20171105150989413916140.png

Scene场景,用于保存布局中所有View的属性值,创建Scene的方式可以通过getSceneForLayout方法

getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)

比如:

mScene0 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene0, getContext());
mScene1 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene1, getContext());

也可以直接new Scene(ViewGroup sceneRoot, View layout)

View view0 = inflater.inflate(R.layout.scene0, container, false);
View view1 = inflater.inflate(R.layout.scene1, container, false);
mScene0 = new Scene(mSceneRoot, view0);
mScene1 = new Scene(mSceneRoot, view1);

两种方式都需要传SceneRoot,即该场景的根节点。

Transition

20171105150989419933684.png
20171105150989419933684.png

Transition过渡动画,前面创建了两个场景,分别保存了视图的一些属性,比如Visibility、position等,Transition就是对于这些属性值的改变定义过渡的效果。从上图可以看到系统内置了一些常用的Transition,Transition的创建可以通过加载xml,如:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

然后在代码中:

Transition mFadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

或者直接在代码中:

Transition mFadeTransition = new Fade();

TransitionManager

TransitionManeger用于将Scene和Transition联系起来,它提供了一系列的方法如setTransition(Scene fromScene, Scene toScene, Transition transition)指明起始场景和结束场景、他们的过渡动画是什么,go(Scene scene, Transition transition),到指定的场景所使用的过渡动画是什么,beginDelayedTransition(ViewGroup sceneRoot, Transition transition),在当前场景到下一帧的过渡效果是什么。比如这里使用go()方法,效果:

2017110515098942555933.gif
2017110515098942555933.gif

注意这里两个Scene中红绿两个方块除了位置和大小不一样,id是一致的,transition记录下两个Scene前后属性值,根据属性值的改变执行过渡动画,默认情况下对SceneRoot下的所有View执行动画效果,我们可以通过Transition.addTarget和removeTarget方法选择性添加或移除执行动画的View。

常用API

有时候我们只想改变当前已展示的视图层级中View的状态,可以通过beginDelayedTransition实现,下面列举系统内置的Transition的使用。

AutoTransition

AutoTransition默认的动画效果,对应xml tag为autoTransition

其实是以下几个动画组合顺序执行:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

在代码中使用:

TransitionManager.beginDelayedTransition(mRoot, new AutoTransition());
        if (mTextView.getVisibility() != View.VISIBLE) {
            mTextView.setVisibility(View.VISIBLE);
        } else {
            mTextView.setVisibility(View.GONE);
        }

20171105150989430739154.gif
20171105150989430739154.gif

ChangeBounds

ChangeBounds对应xml tag为changeBounds,根据前后布局界限的变化执行动画

TransitionManager.beginDelayedTransition(mRoot, new ChangeBounds());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
if ((lp.gravity & Gravity.START) == Gravity.START) {
    lp.gravity = Gravity.BOTTOM | Gravity.END;
} else {
    lp.gravity = Gravity.TOP | Gravity.START;
}
mTarget.setLayoutParams(lp);

2017110515098943515468.gif
2017110515098943515468.gif

ChangeClipBounds

ChangeClipBounds对应xml tag为changeClipBounds,作用对象:View的getClipBounds()值

Rect BOUNDS = new Rect(20, 20, 100, 100);
TransitionManager.beginDelayedTransition(mRoot, new ChangeClipBounds());
if (BOUNDS.equals(ViewCompat.getClipBounds(mImageView))) {
    ViewCompat.setClipBounds(mImageView, null);
} else {
    ViewCompat.setClipBounds(mImageView, BOUNDS);
}

20171105150989440946179.gif
20171105150989440946179.gif

ChangeImageTransform

对应xml tag为changeImageTransform,作用对象:ImageView的matrix

TransitionManager.beginDelayedTransition(mRoot, new ChangeImageTransform());
mImageView.setScaleType(ImageView.ScaleType.XXX);

2017110515098944414343.gif
2017110515098944414343.gif

ChangeScroll

对应xml tag为changeScroll,作用对象:View的scroll属性值

TransitionManager.beginDelayedTransition(mRoot, new ChangeScroll());
mTarget.scrollBy(-100, -100);

20171105150989447884273.gif
20171105150989447884273.gif

ChangeTransform

对应xml tag 为changeTransform,作用对象:View的scale和rotation

TransitionManager.beginDelayedTransition(mRoot, new ChangeTransform());
if (mContainer2.getChildCount() > 0) {
    mContainer2.removeAllViews();
    showRedSquare(mContainer1);
} else {
    mContainer1.removeAllViews();
    showRedSquare(mContainer2);
    mContainer2.getChildAt(0).setRotation(45);
}
private void showRedSquare(FrameLayout container) {
        final View view = LayoutInflater.from(getContext())
                .inflate(R.layout.red_square, container, false);
        container.addView(view);
}

20171105150989451859396.gif
20171105150989451859396.gif

Explode

对应xml tag为explode,作用对象:View的Visibility

TransitionManager.beginDelayedTransition(mRoot, new Explode());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
    view.setVisibility(vis);
}

20171105150989454127663.gif
20171105150989454127663.gif

Fade

对应xml tag为fade,作用对象:View的Visibility

可以在初始化时指定IN或者OUT分别对应淡入和淡出,也可以通过fade.setMode方法设置,若不指定默认为淡入淡出效果

TransitionManager.beginDelayedTransition(mRoot, new Fade());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
    view.setVisibility(vis);
}

20171105150989462426664.gif
20171105150989462426664.gif

Slide

对应xml tag为slide,作用对象:View的Visibility

可以初始化时传入Gravity.XX,也可以通过slide.setSlideEdge方法设置,默认方向为Gravity.BOTTOM

TransitionManager.beginDelayedTransition(mRoot, new Slide());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
    view.setVisibility(vis);
}

20171105150989465258549.gif
20171105150989465258549.gif

TransitionSet

对应xml tag为transitionSet

可以在代码中创建transitionSet如:

mTransition = new TransitionSet();
mTransition.addTransition(new ChangeImageTransform());
mTransition.addTransition(new ChangeTransform());
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
        if (mInnerFrame.getChildCount() > 0) {
            mInnerFrame.removeAllViews();
            addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
        } else {
            mOuterFrame.removeViewAt(1);
            addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
                    FrameLayout.LayoutParams.MATCH_PARENT);
        }

也可以通过加载xml布局创建transitionSet:

xml布局长这样:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together">
    <changeImageTransform/>
    <changeTransform/>
</transitionSet>

通过transitionOrdering属性设置动画执行的顺序,together表示同时执行,sequential表示顺序执行,在代码中可以调用TransitionSet的setOrdering(int)方法,属性值传ORDERING_SEQUENTIAL或者ORDERING_TOGETHER

在代码中:

mTransition = (TransitionSet) TransitionInflater.from(getContext()).inflateTransition(R.transition.transition);
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
        if (mInnerFrame.getChildCount() > 0) {
            mInnerFrame.removeAllViews();
            addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
        } else {
            mOuterFrame.removeViewAt(1);
            addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
                    FrameLayout.LayoutParams.MATCH_PARENT);
        }

这里结合changeImageTransform和changeTransform,效果如下:

20171105150989470836625.gif
20171105150989470836625.gif

PathMotion

20171105150989473563526.png
20171105150989473563526.png

Transition的辅助工具,以path的方式指定过渡效果,两个具体实现类ArcMotion和PatternPathMotion,看下ArcMotion的效果

mTransition = new AutoTransition();
mTransition.setPathMotion(new ArcMotion());
TransitionManager.beginDelayedTransition(mRoot, mTransition);
        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
        if ((lp.gravity & Gravity.START) == Gravity.START) {
            lp.gravity = Gravity.END | Gravity.BOTTOM;
        } else {
            lp.gravity = Gravity.START | Gravity.TOP;
        }
        mTarget.setLayoutParams(lp);

20171105150989476785110.gif
20171105150989476785110.gif

它的运动轨迹是条曲线,有兴趣的可以研究下它的实现算法,在源码中有个很萌的图如下:

20171105150989479123778.png
20171105150989479123778.png

自定义Transition

除了系统内置的Transition,我们还可以自定义Transition效果,需要继承Transition

public class CustomTransition extends Transition {
    @Override
    public void captureStartValues(TransitionValues values) {}
    @Override
    public void captureEndValues(TransitionValues values) {}
    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues,
                                   TransitionValues endValues) {}
}

其工作原理是在captureStartValues和captureEndValues中分别记录View的属性值,官网建议确保属性值不冲突,属性值的命名格式参考:

package_name:transition_name:property_name

在createAnimator中创建动画,对比属性值的改变执行动画效果,如自定义修改颜色动画效果:

20171105150989483061249.gif
20171105150989483061249.gif

在两个Scene中使用自定义过渡动画,效果如下:

20171105150989484813223.gif
20171105150989484813223.gif

Note

  1. Android 版本在4.0(API Level 14)到4.4.2(API Level 19)使用Android Support Library’s

  2. 对于 SurfaceView可能不起效果,因为SurfaceView的实例是在非UI线程更新的,因此会造成和其他视图动画不同步。

  3. 某些特定的转换类型在应用到TextureView时可能不会产生所需的动画效果。

  4. 继承自AdapterView的如ListView,与该框架不兼容。

  5. 不要对包含文本的视图的大小进行动画

示例过渡动画

在界面过渡上,Transition分为不带共享元素的Content Transition和带共享元素的ShareElement Transition。

Content Transition

先看下content transition的一个例子,在Google Play Games上的应用:

20171105150989496665009.gif
20171105150989496665009.gif

在经过学习后我们也可以设计出类似的效果,首先需要了解在界面过渡中涉及到的一些重要方法,从ActivtyA调用startActivity方法唤起ActivityB,到ActivityB按返回键返回ActivityA涉及到与Transition有关的方法

2017110515098950616029.png
2017110515098950616029.png

  • ActivityA.exitTransition()
  • ActivityB.enterTransition()

20171105150989510757346.png
20171105150989510757346.png

  • ActivityB.returnTransition()
  • ActivityA.reenterTransition()

因此,只要我们在对应的方法中设置了Transition就可以了。在默认没有设置对应Transition的情况下,Material-theme应用的exitTransition为null,enterTransition为Fade,如果reenterTransition和returnTransition未设定,则分别对应exitTransition和enterTransition。

使用

在style中添加android:windowContentTransitions 属性启用窗口内容转换(Material-theme应用默认为true),指定该Activity的Transition

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- enable window content transitions -->
    <item name="android:windowContentTransitions">true</item>

    <!-- specify enter and exit transitions -->
    <!-- options are: explode, slide, fade -->
    <item name="android:windowEnterTransition">@transition/change_image_transform</item>
    <item name="android:windowExitTransition">@transition/change_image_transform</item>
</style>

也可以在代码中指定

// inside your activity (if you did not enable transitions in your theme)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
// set an enter transition
getWindow().setEnterTransition(new Explode());
// set an exit transition
getWindow().setExitTransition(new Explode());

然后启动Acticity

startActivity(intent,
              ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

例子

这里在代码中指定ActivityA的exitTransition:

private void setupTransition() {
        Slide slide = new Slide(Gravity.LEFT);
        slide.setDuration(1000);
        slide.setInterpolator(new FastOutSlowInInterpolator());
        getWindow().setExitTransition(slide);
    }

使用xml方式指定ActivityB的enterTransition:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <slide
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"
        android:slideEdge="bottom">
        <targets>
            <target android:targetId="@id/content_container"/>
        </targets>
    </slide>
    <slide
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"
        android:slideEdge="top">
        <targets>
            <target android:targetId="@id/image_container"/>
        </targets>
    </slide>
</transitionSet>

运行效果如下:

20171105150989524969946.gif
20171105150989524969946.gif

上图动画有两个问题:

  1. ActivityA的exitTransition还没完全走完ActivityB的enterTransition就执行了,ActivityB的returnTransition还没完全走完ActivityA的reenterTransition就执行了;
  2. 状态栏和导航栏的动画不太协调;

问题1是因为默认情况下enter/return transition会比exit/reenter transition完全结束前稍微快一点运行,如果想让前者完全运行完后者再进来,可以在代码中调用Window的

setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)

或者在xml中设置

<item name="android:windowAllowEnterTransitionOverlap">false</item> 
<item name="android:windowAllowReturnTransitionOverlap">false</item>

运行如下:

20171105150989532889905.gif
20171105150989532889905.gif

再看下问题2,默认情况下状态栏和标题栏也会参与动画(如果有导航栏也会,测试机默认木有导航栏),当我们把xxxoverlap属性设为false后就看得比较明显了,如果不想让它们参与动画通过excludeTarget()将其排除,在代码中:

private void setupTransition() {
    Slide slide = new Slide(Gravity.LEFT);
    slide.setDuration(1000);
    slide.setInterpolator(new FastOutSlowInInterpolator());
    slide.excludeTarget(android.R.id.statusBarBackground, true);
    slide.excludeTarget(android.R.id.navigationBarBackground, true);
    slide.excludeTarget(R.id.appbar,true);
    getWindow().setExitTransition(slide);
}

或者在xml中:

<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:slideEdge="left"
    android:interpolator="@android:interpolator/fast_out_slow_in"
    android:duration="1000">

    <targets>
        <!-- if using a custom Toolbar container, specify the ID of the AppBarLayout -->
        <target android:excludeId="@id/appbar" />
        <target android:excludeId="@android:id/statusBarBackground"/>
        <target android:excludeId="@android:id/navigationBarBackground"/>
    </targets>

</slide>

效果如下:

2017110515098956444650.gif
2017110515098956444650.gif

具体流程

ActivityA startActivity()

  1. 确定需要执行exit Transition的target View
  2. Transition的captureStartValues()获取target View Visibility的值(此时为VISIBLE)
  3. 将target View Visibility的值设为INVISIBLE
  4. Transition的captureEndValues()获取target View Visibility的值(此时为INVISIBLE)
  5. Transition的createAnimator()根据前后Visibility的属性值变化创建动画

ActivityB Activity 开始

  1. 确定需要执行enter Transition的target View
  2. Transition的captureStartValues()获取获取target View Visibility的,初始化为INVISIBLE
  3. 将target View Visibility的值设为VISIBLE
  4. Transition的captureEndValues()获取target View Visibility的值(此时为VISIBLE)
  5. Transition的createAnimator()根据前后Visibility的属性值变化创建动画

ShareElement Transition

shareElement Transition的例子

20171105150989573322645.gif
20171105150989573322645.gif

shareElement Transition指的是共享元素从activity/fragment到其他activity/fragment时的动画

20171105150989577752186.png
20171105150989577752186.png

有了上面的分析看名字应该也猜得出方法对应的功能了,如果没有设置exit/enter shared element transitions,默认为 @android:transition/move,上面的Content Transition是根据Visibility的变化创建动画,而shareElement Transition是根据大小,位置,和外观的变化创建动画,如chanageBounds、changeTransform、ChangeClipBounds、 ChangeImageTransform等,具体API使用和效果可以参考上篇。指定shareElement Transition可以通过代码形式:

getWindow().setSharedElementEnterTransition();
getWindow().setSharedElementExitTransition();
getWindow().setSharedElementReturnTransition();
getWindow().setSharedElementReenterTransition();

也可以通过xml形式:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- specify shared element transitions -->
    <item name="android:windowSharedElementEnterTransition">
      @transition/change_image_transform</item>
    <item name="android:windowSharedElementExitTransition">
      @transition/change_image_transform</item>
</style>

然后启动Acticity

Intent intent = new Intent(this, DetailsActivity.class);
// Pass data object in the bundle and populate details activity.
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
ActivityOptionsCompat options = ActivityOptionsCompat.
    makeSceneTransitionAnimation(this, (View)ivProfile, "profile");
startActivity(intent, options.toBundle());

在布局文件中对于要共享的View添加android:transitionName且保持一致,如果要共享的View有点多,可以通过Pair,Pair 存储着共享View和View的名称,使用如下

Intent intent = new Intent(context, DetailsActivity.class);
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
Pair<View, String> p1 = Pair.create((View)ivProfile, "profile");
Pair<View, String> p2 = Pair.create(vPalette, "palette");
Pair<View, String> p3 = Pair.create((View)tvName, "text");
ActivityOptionsCompat options = ActivityOptionsCompat.
    makeSceneTransitionAnimation(this, p1, p2, p3);
startActivity(intent, options.toBundle());

例子

在ActivityB的theme中添加SharedElementEnterTransition

<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform
</item>

change_image_transform.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"/>
    <changeImageTransform
        android:duration="1000"
        android:interpolator="@android:interpolator/fast_out_slow_in"/>
</transitionSet>

执行效果:

20171105150989590151493.gif
20171105150989590151493.gif

具体流程

从图上看,好像图片是从一个ActivityA"传递"到另一个ActivityB,实际上真正负责绘制都发生在ActivityB上:

  1. ActivityA调用startActivity()后ActivityB处于透明状态
  2. Transition收集ActivityA中共享View的初识状态,并传递给ActivityB
  3. Transition收集ActivityB中共享View的最终状态
  4. Transition根据状态改变创建动画
  5. Transition隐藏ActivityA,随着ActivityB的共享View运动到指定位置,ActivityB的背景在ActivityA上淡入,并随着动画完成而完全可见。

我们可以通过修改Activity背景淡入淡出时间来验证,在ActivityB中加入

getWindow().setTransitionBackgroundFadeDuration(2000);

为了更直观,把ActivityA的exitTransition先注释掉,运行效果:

20171105150989597967932.gif
20171105150989597967932.gif

可以看到,ActivityB确实像盖在ActivityA上,这里用到了 ViewOverlay,原理简单来说就是在其他View上draw,共享View利用该技术可以实现画在其他View上。我们可以通过Window的setSharedElementsUseOverlay(false)来关闭该功能,不过这样一来会使最终结果和你预想的不一致,默认该值为true。

延迟加载

上面分析Transition会获取共享视图前后的状态值来创建动画,如果我们的图片是网上下载的,那么很有可能图片的准确大小需要下载下来才能确定,Activity Transitions API提供了一对方法暂时推迟过渡,直到我们确切地知道共享元素已经被适当的渲染和放置。在onCreate中调用postponeEnterTransition()(API >= 21)或者supportPostponeEnterTransition()(API < 21)延迟过渡;当图片的状态确定后,调用startPostponedEnterTransition()(API >= 21)或supportStartPostponedEnterTransition()(API < 21)恢复过渡,常见处理:

// ... load remote image with Glide/Picasso here

supportPostponeEnterTransition();
ivBackdrop.getViewTreeObserver().addOnPreDrawListener(
    new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            ivBackdrop.getViewTreeObserver().removeOnPreDrawListener(this);
            supportStartPostponedEnterTransition();
            return true;
        }
    }
);
2017/10/18 posted in  Android

Java-你应该知道的JDK知识

前言

无论是从事Javaee开发或者是Android开发,JDK的基础知识都尤为重要。我们在代码里经常使用ArrayList、HashMap等,但却很少思考为什么是使用它,使用的时候需要注意什么。甚至有可能去面试的时候,人家一问HashMap的实现原理,但却只知道put和get,非常尴尬。

所以为了开发更高质量的程序,写出更优秀的代码,还是需要好好研究一下JDK的一些关键源码。本文主要对JDK进行一些重要的的知识的梳理及整理,便于学习及复习。

基础知识

基础数据类型

变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。
内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据

类型 默认值
byte 8(1字节) 0
short 16(2字节) 0
int 32(4字节) 0
long 64(8字节) 0L
float 32(4字节) 0.0f
double 64(8字节) 0.0d
boolean 1 false
char 16 位 Unicode 字符 “”

equal hashcode ==的区别

== 内存地址比较
equal Object默认内存地址比较,一般需要复写
hashcode 主要用于集合的散列表,Object默认为内存地址,一般不用设置,除非作用于散列集合。

hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。当equals方法被重写时,通常有必要重写 hashCode 方法。但hashCode相等,不一定equals()

String、StringBuffer与StringBuilder的区别。

Java 平台提供了两种类型的字符串:String和StringBuffer / StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer和StringBulder类表示的字符串对象可以直接进行修改。StringBuilder是JDK1.5引入的,它和StringBuffer的方法完全相同,区别在于它是单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer略高。

Java的四种引用,强弱软虚,用到的场景。

JDK1.2之前只有强引用,其他几种引用都是在JDK1.2之后引入的.

强引用(Strong Reference) 最常用的引用类型,如Object obj = new Object(); 。只要强引用存在则GC时则必定不被回收。

软引用(Soft Reference) 用于描述还有用但非必须的对象,当堆将发生OOM(Out Of Memory)时则会回收软引用所指向的内存空间,若回收后依然空间不足才会抛出OOM。一般用于实现内存敏感的高速缓存。 当真正对象被标记finalizable以及的finalize()方法调用之后并且内存已经清理, 那么如果SoftReference object还存在就被加入到它的 ReferenceQueue.只有前面几步完成后,Soft Reference和Weak Reference的get方法才会返回null

弱引用(Weak Reference) 发生GC时必定回收弱引用指向的内存空间。 和软引用加入队列的时机相同

虚引用(Phantom Reference) 又称为幽灵引用或幻影引用,虚引用既不会影响对象的生命周期,也无法通过虚引用来获取对象实例,仅用于在发生GC时接收一个系统通知。 当一个对象的finalize方法已经被调用了之后,这个对象的幽灵引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了. 虚引用和软引用和弱引用都不同,它会在内存没有清理的时候被加入引用队列.虚引用的建立必须要传入引用队列,其他可以没有

Java集合框架

2017110515098931725903.png
2017110515098931725903.png

Collection是List、Set等集合高度抽象出来的接口,它包含了这些集合的基本操作,它主要又分为两大部分:List和Set。

List接口通常表示一个列表(数组、队列、链表、栈等),其中的元素可以重复,常用实现类为ArrayList和LinkedList,另外还有不常用的Vector。另外,LinkedList还是实现了Queue接口,因此也可以作为队列使用。

Set接口通常表示一个集合,其中的元素不允许重复(通过hashcode和equals函数保证),常用实现类有HashSet和TreeSet,HashSet是通过Map中的HashMap实现的,而TreeSet是通过Map中的TreeMap实现的。另外,TreeSet还实现了SortedSet接口,因此是有序的集合(集合中的元素要实现Comparable接口,并覆写Compartor函数才行)。 我们看到,抽象类AbstractCollection、AbstractList和AbstractSet分别实现了Collection、List和Set接口,这就是在Java集合框架中用的很多的适配器设计模式,用这些抽象类去实现接口,在抽象类中实现接口中的若干或全部方法,这样下面的一些类只需直接继承该抽象类,并实现自己需要的方法即可,而不用实现接口中的全部抽象方法。

Map是一个映射接口,其中的每个元素都是一个key-value键值对,同样抽象类AbstractMap通过适配器模式实现了Map接口中的大部分函数,TreeMap、HashMap、WeakHashMap等实现类都通过继承AbstractMap来实现,另外,不常用的HashTable直接实现了Map接口,它和Vector都是JDK1.0就引入的集合类。

Iterator是遍历集合的迭代器(不能遍历Map,只用来遍历Collection),Collection的实现类都实现了iterator()函数,它返回一个Iterator对象,用来遍历集合,ListIterator则专门用来遍历List。而Enumeration则是JDK1.0时引入的,作用与Iterator相同,但它的功能比Iterator要少,它只能再Hashtable、Vector和Stack中使用。

Arrays和Collections是用来操作数组、集合的两个工具类,例如在ArrayList和Vector中大量调用了Arrays.Copyof()方法,而Collections中有很多静态方法可以返回各集合类的synchronized版本,即线程安全的版本,当然了,如果要用线程安全的结合类,首选Concurrent并发包下的对应的集合类。

Collection List Set Map 区别

接口 是否有序 允许元素重复
collection
List
AbstractSet
HashSet
TreeSet 是(用二叉树排序)
AbstractMap 使用 key-value 来映射和存储数据, Key 必须惟一, value 可以重复
HashMap 使用 key-value 来映射和存储数据, Key 必须惟一, value 可以重复
TreeMap 是(用二叉树排序) 使用 key-value 来映射和存储数据, Key 必须惟一, value

常用集合分析

集合 主要算法 源码分析
ArrayList 基于数组的List,封装了动态增长的Object[] 数组 链接
Stack 是Vector 的子类,栈 的结构(后进先出) 链接
LinkedList 实现List,Deque;实现List,可以进行队列操作,可以通过索引来随机访问集合元素;实现Deque,也可当作双端队列,也可当作栈来使用 链接
HashMap 基于哈希表的 Map 接口的实现, 使用顺序存储及链式存储的结构 链接
LinkedHashMap LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同 链接
TreeMap TreeMap的实现是红黑树算法的实现,支持排序 链接

并发

Lists

  • ArrayList——基于泛型数组
  • LinkedList——不推荐使用
  • Vector——已废弃(deprecated)
  • CopyOnWriteArrayList——几乎不更新,常用来遍历

Queues / deques

  • ArrayDeque——基于泛型数组
  • Stack——已废弃(deprecated)
  • PriorityQueue——读取操作的内容已排序
  • ArrayBlockingQueue——带边界的阻塞式队列
  • ConcurrentLinkedDeque / ConcurrentLinkedQueue——无边界的链表队列(CAS)
  • DelayQueue——元素带有延迟的队列
  • LinkedBlockingDeque / LinkedBlockingQueue——链表队列(带锁),可设定是否带边界
  • LinkedTransferQueue——可将元素transfer进行w/o存储
  • PriorityBlockingQueue——并发PriorityQueue
  • SynchronousQueue——使用Queue接口进行Exchanger

Maps

  • HashMap——通用Map
  • EnumMap——键使用enum
  • Hashtable——已废弃(deprecated)
  • IdentityHashMap——键使用==进行比较
  • LinkedHashMap——保持插入顺序
  • TreeMap——键已排序
  • WeakHashMap——适用于缓存(cache)
  • ConcurrentHashMap——通用并发Map
  • ConcurrentSkipListMap——已排序的并发Map

Sets

  • HashSet——通用set
  • EnumSet——enum Set
  • BitSet——比特或密集的整数Set
  • LinkedHashSet——保持插入顺序
  • TreeSet——排序Set
  • ConcurrentSkipListSet——排序并发Set
  • CopyOnWriteArraySet——几乎不更新,通常只做遍历

总结

Set的选择

  1. HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet
  2. 对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由维护链表所带来的开销造成的。不过,因为有了链表的存在,遍历LinkedHashSet会更快
  3. EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素
  4. HashSet、TreeSet、EnumSet都是"线程不安全"的,通常可以通过Collections工具类的synchronizedSortedSet方法来"包装"该Set集合。
    SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

List 选择

  1. java提供的List就是一个"线性表接口",ArrayList(基于数组的线性表)、LinkedList(基于链的线性表)是线性表的两种典型实现
  2. Queue代表了队列,Deque代表了双端队列(既可以作为队列使用、也可以作为栈使用)
  3. 因为数组以一块连续内存来保存所有的数组元素,所以数组在随机访问时性能最好。所以的内部以数组作为底层实现的集合在随机访问时性能最好。
  4. 内部以链表作为底层实现的集合在执行插入、删除操作时有很好的性能
  5. 进行迭代操作时,以链表作为底层实现的集合比以数组作为底层实现的集合性能好
  6. 当要大量的插入,删除,应当选用LinkedList;当需要快速随机访问则选用ArrayList;

Map 的选择

  1. HashMap和Hashtable的效率大致相同,因为它们的实现机制几乎完全一样。但HashMap通常比Hashtable要快一点,因为Hashtable需要额外的线程同步控制
  2. TreeMap通常比HashMap、Hashtable要慢(尤其是在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对
  3. 使用TreeMap的一个好处就是: TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作
  4. HahMap 是利用hashCode 进行查找,而TreeMap 是保持者某种有序状态
  5. 所以,插入,删除,定位操作时,HashMap 是最好的选择;如果要按照自然排序或者自定义排序,那么就选择TreeMap
2017/10/17 posted in  Android

Android-粒子变幻、隧道散列、组合文字

效果视频 & 图片

第一个视频,无散列

20171105150982542062457.gif
20171105150982542062457.gif

第二个视频,具备散列

20171105150982545476983.gif
20171105150982545476983.gif

20171105150982548447008.png
20171105150982548447008.png

20171105150982550163583.png
20171105150982550163583.png

概述

跟随早前开源的 XView (github.com/af913337456…) 项目,本次在原基础上添加了 粒子变幻 自定义View。目前我在代码里面的设置它可以做到:

  1. 根据你输入文字,将被粒子组合而成。
  2. 粒子流具备多种属性,目前我拓展了缩放,圆形与矩形,墙壁碰撞,等等。
  3. 粒子每个互不影响,可以分批设置粒子特性,视频中就有 方形 和 圆形
  4. 所有的半径,坐标什么的参数都是可自定义的。
  5. 因为锚边是根据 bitmap 而来的,也就是说,你可以输入图片,然后由粒子组合
  6. XView 项目早前已经开源了碰撞球,可以加入粒子相互碰撞

原理及其难点

  1. 根据 bitmap 找出文字或图像的边。这步骤要减少 o(n)
  2. 根据边路径,进行粒子填充
  3. 变幻算法,例如运动中的缩放
  4. 高效率的刷新,摒弃 View,采用 SurfaceView

部分代码简述

调用

// 粒子变幻
particleView.setConfigAndRefreshView(
    new ParticleView.Config()
            .setCanvasWidth(
                    // 设置画布宽度
                    getWindowManager().getDefaultDisplay().getWidth()
            )
            .setCanvasHeight(800) // 设置画布高度
            .setParticleRefreshTime(50) // 设置每帧刷新间隔
            .set_x_Step(15) // 设置 x 轴每次取像素点的间隔
            .set_y_Step(19) // 设置  轴每次取像素点的间隔
            .setParticleCallBack(
                new ParticleView.ParticleCallBack() {
                    @Override
                    public ParticleView.Particle setParticle(ParticleView.Particle p, int index, int x, int y) {
                        p.setX(x); // 设置获取回来的 x 为该 粒子的x坐标
                        p.setY(y); // 设置获取回来的 y 为该 粒子的y坐标
                        p.setIsZoom(true);  // 设置当前颗粒子是否启动缩放
                        p.setRadiusMax(12); // 设置当前颗粒子最大的缩放半径
                        p.setRadius(12);    // 设置当前颗粒子默认的半径

                        /** 下面的 %3 是我演示 分批次 显示不同效果而设置 **/
                        if(index % 3==0){
                            p.setRectParticle(true); // 这个粒子是 正方形 的
                            p.setIsHash(  // 设置它是否是散列的,即是随机移动
                                    true,
                                    new Random().nextInt(30)-15, // x 速率
                                    new Random().nextInt(30)-15  // y 速率
                            );
                        }
                        return p; // 返回这个粒子
                    }

                    @Override
                    public boolean drawText(Bitmap bg,Canvas c) {
                        /** 这里就是我们要自定义显示任意文字的地方 */
                        MainActivity.this.drawText(bg,c,s);
                        return true; /** 告诉它不要使用默认的 txt */
                    }
                }
            )
);

源码地址

github.com/af913337456…

2017/10/15 posted in  Android

Android-LayoutInflater(布局加载器)

前言

对于LayoutInflater之前一直只会用,却不知道LayoutInflater的加载原理,每次直接

LayoutInflater.from(context).inflate(R.layout.activity_test, root, false);

//不行就这样,反正有一种能实现我要的效果
LayoutInflater.from(context).inflate(R.layout.activity_test, null);

所以,想要趁这个整理博客的机会,顺便把LayoutInflater的内容好好学习学习。

概述

  • LayoutInflater的常见使用场景
  • LayoutInflater的介绍
  • LayoutInflater相关介绍中的相关概念分析

LayoutInflater的常见使用场景

在介绍之前,我们先回一下,我们在哪些地方都使用过LayoutInflater:

在Activity中

LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(R.layout.activity_main, null);

在Fragment中

View view = inflater.inflate(R.layout.fragment_guide_one, container, false);
return view;

在Adapter中

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = LayoutInflater.from(convertView.getContext()).inflate(R.layout.activity_main, parent, false);
    return view;
}

在某些特殊情况下,需要使用LayoutInflater,我们是这样获得它的

LayoutInflater inflater =(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

上述的使用,是我们平常常见的使用方式,而这些场景都有一个特点,因为这些场景都需要将一个XML布局文件转化成View,所以准确的说LayoutInflater的主要功能来说就是布局加载。

其实LayoutInflater还有一些扩展操作,可以通过我们自定义的方式来实现,在后面的实战篇会介绍。

LayoutInflater的介绍

对于LayoutInflater的介绍性质的内容,博主认为,在网上查的任何内容,都不如查阅源码,API来的靠谱一些,因为API才是第一手的介绍资料,而且Android的源码中介绍的也比较完善。

LayoutInflater属于 android.view包下,在LayoutInflater的头部有一段关于LayoutInflater的介绍:

20171105150981598486071.png
20171105150981598486071.png

由于篇幅原因,这里只截取了一部分图片,总结一下:

  • LayoutInflater的主要作用将XML文件实例化成相应的View对象
  • LayoutInflater在Android开发过程中,获取的方式不是直接new出来的,都是经过这两个方法得到的关联上下文的LayoutInflater:
//在Activity中
LayoutInflater inflater = Activity#getLayoutInflater()
//其他情况
LayoutInflater inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
  • 如果想使用新的LayoutInflater来加载View,需要使用cloneInContext(),而在新的LayoutInflater需要调用setFactory()设置视图处理器
  • 由于性能的原因,XML文件的预处理是在Build过程中进行的。
  • LayoutInflater不能加载未编译的XML文件,而且LayoutInflater只能加载,通过XmlPullParser解析的R文件资源。

LayoutInflater介绍相应的解释

经过上面的总结,大家对LayoutInflater有一个大致的认识,为了大家不是太懵逼,让我一一解释一波。

LayoutInflater的主要作用将XML文件实例化成相应的对象

其实,大家在使用LayoutInflater时,也会注意到无非就是将布局资源通过LayoutInflater转换为相对应的View,然后在进行一些其他操作,就是LayoutInflater常见场景中的几种情况:

View view = inflater.inflate(R.layout.fragment_guide_one, container, false);
return view;

LayoutInflater在Android开发过程中,不是通过new出来获取到的?

在上述场景中,除了介绍的两种方式Activity#getLayoutInflater(),以及getSystemService(),大家发现常见场景中还使用了

LayoutInflater inflater =LayoutInflater.from(convertView.getContext());

其实LayoutInflater.from()这个方法是官方帮我们封装了一层而已,底层还是调用getSystemService()方法,目的是使LayoutInflater与Context对象相绑定:

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

如果想使用新的LayoutInflater来加载,需要使用cloneInContext(),而在新的LayoutInflater需要调用setFactory()设置视图处理器

正常来说,这种使用方式的使用场景现在也是比较多的,比如:

  1. 批量获取XML中自定义的属性
  2. 动态换肤的效果
  3. 动态改变布局中的元素

这些都是通过LayoutInflater中的Factory来实现的,而介绍这部分内容会在实战篇来介绍。

由于性能的原因,XML文件的预处理是在Build过程中进行的

举个例子,在编写XML布局资源时,如果漏写了结束符号,或者一些奇怪的操作,在运行程序之前的Build(构建阶段),就会弹出报错。

这里故意将结束符,写错

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:textSize="20sp" /

这里就会收到报错信息提示,每个XML都会有一个预编译的过程,这个过程发生在构建阶段(Build),而不是运行时。

20171105150981625428282.png
20171105150981625428282.png

LayoutInflater只能加载通过XmlPullParser解析的R文件资源

这里的R文件资源就是指这些资源文件

例如:

R.layout.xxxx
R.drawable.xxxx
R.color.xxx
R.string.xxx

二级概述

  • Activity 的 getSystemService的实现过程
  • LayoutInflater 如果将布局资源转换为 View 的过程
  • LayoutInflater的 Factory,Factory2是什么,在解析过程中的作用是什么?
  • LayoutInflater 的 inflater 方法的各个参数的含义,不同的情况的含义

LayoutInflater的构造方法

protected LayoutInflater(Context context) {
    mContext = context;
}

这种是LayoutInflater常规的构造方法,将Context传入,最后生成的LayoutInflater与对应的Context相绑定。

protected LayoutInflater(LayoutInflater original, Context newContext) {
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
}

而这种构造方法来说,只是复制原LayoutInflater的内容,然后将Context对象替换,一般来说只会在cloneInContext()方法中使用。

LayoutInflater#form()方法分析

根据介绍篇的内容,LayoutInflater在Android开发中一般是通过

context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

LayoutInflater.from(context);

因为第一种方式,已经是LayoutInflater介绍中声明获取的方式之一,那么这里我们看一下LayoutInflater#form的方法。

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

从源码上看,LayoutInflater#form()方法内部也是通过getSystemService()方法获得,那么接下来我们看一下context#getSystemService()这个方法:

public abstract Object getSystemService(@ServiceName @NonNull String name);

发现这个只是一个抽象方法,而我们知道Activity也是Context的一个实现。

Activity#getSystemService()这个方法:

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
            "System services not available to Activities before onCreate()");
    }
    //获取WindowManager
    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
        //系统的搜索框SearchManager
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

从上面看到,在Activity中只处理了两种类型的服务,分别是获取WindowManager、获取SearchManager,那我们接着看其父类的SystemService()方法:

@Override
public Object getSystemService(String name) {
    //找到我们要的东西,注意这是个单例
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

在Activity的父类即ContextThemeWrapper的getSystemService()方法中,我们发现了LayoutInflater的创建过程,从上面的代码我们可以看出:

每个Activity内包含的LayoutInflater是一个单例。

Activity创建LayoutInflater时,是先使用最原始的BaseContext创建,然后在将Activity的父类ContextThemeWrapper的信息通过cloneInContext()方法与其绑定。

然后我们在看下LayoutInflater的cloneInContext的实现:

`public abstract LayoutInflater cloneInContext(Context newContext);`

先看下,这个方法的介绍:

![2017110515098197795785.png](http://ohtrrgyyd.bkt.clouddn.com/2017110515098197795785.png)

这个方法通过现有的LayoutInflater创建一个新的LayoutInflater副本,唯一变化的地方是指向不同的上下文对象。

在ContextThemeWrapper通过这个方法创建的新的LayoutInflater还包含了主题的信息。

在ContextThemeWrapper中使用cloneInContext是想将更多的信息,赋予LayoutInflater中,与其相互绑定。

# Activity中LayoutInflater创建

对于Activity的LayoutInflater,其实在Activity创建之时就已经创建完成,但是这一块内容属于FrameWork层的内容,博主道行太浅了,只想带大家看下from这个方法的实现过程。

这里如果大家想了解可以参考下这篇文章

[LayoutInflater源码解析](http://blog.csdn.net/u014486880/article/details/50707672)

而Activity#getLayoutInflater方法:

```java
@NonNull
public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
}

这个Window对象即PhoneWindow,此时创建出来的LayoutInflater即PhoneLayoutInflater。

这里给大家看下PhoneLayoutInflater的cloneInContext()方法:

```java
public LayoutInflater cloneInContext(Context newContext) {
    return new PhoneLayoutInflater(this, newContext);
}

protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
    super(original, newContext);
}

可以发现PhoneLayoutInflater中cloneInContext()的实现,调用了第二个构造方法。

这里在Android Studio是无法查阅的,有条件的可以下载源码,如果下载源码麻烦,可以在这里查阅。

Android源码查看网址

将R.layout.xxx转换为View的过程分析

其实这个过程即LayoutInflater.inflater()这个过程:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

在这个方法中,只是先拿到XmlResourceParser,用于后续节点的解析,我们接着往下看:

这里只看一些关键的信息,具体代码大家自行查看

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {

    //》》》》》》》》》》》》》》》》》第一部分》》》》》》》》》》》》》》》》》》》
    try {
        while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {
        // Empty
        }

        if (type != XmlPullParser.START_TAG) {
            throw new InflateException(parser.getPositionDescription() + ": No start tag found!");
        }

        final String name = parser.getName()
        //》》》》》》》》》》》》》》》》》第二部分》》》》》》》》》》》》》》》》》》》
        if (TAG_MERGE.equals(name)) {
            if (root == null || !attachToRoot) {
                throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true");
            }

            rInflate(parser, root, inflaterContext, attrs, false);
        } else {
            //》》》》》》》》》》》》》》》》》第三部分》》》》》》》》》》》》》》》》》》》
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);

            ViewGroup.LayoutParams params = null;

            if (root != null) {
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    temp.setLayoutParams(params);
                }
            }

            rInflateChildren(parser, temp, attrs, true);

            if (root != null && attachToRoot) {
                root.addView(temp, params);
            }

            if (root == null || !attachToRoot) {
                result = temp;
            }
        }
        return result;
    }
}

第一部分:

这里第一部分的内容,主要是一个XML文件的读取过程,这里有两个判断:

  • 遍历XML内容寻找XML标签的开始的标志或者文档结尾的标志才可以跳出循环。
  • 如果该XML没有开始的标识,则抛出异常。

下面给大家介绍下,几种常见的解析标识:

XmlPullParser.START_DOCUMENT                                    文档开始

XmlPullParser.END_DOCUMENT                                      文档结束

XmlPullParser.START_TAG                                         XML标签的开始

XmlPullParser.END_TAG                                           XML标签的结束

XmlPullParser.TEXT                                              XML标签的内容

第二部分

这部分的一开始先进行了Merge标签的检验,如果发现该节点是Merge,必须满足父View存在,并且与父View绑定的状态。

转换为代码:

root != null && attachToRoot ==true

这里Merge是减少布局层级存在的标签,通常和include标签一起使用,所以其必须存在父View,而且merge标签的内容必须与父View绑定。

这里调用rInflate()方法去解析Merge的标签,而rInflate()方法,在另一篇文章已经单独分析。

Android 中LayoutInflater(布局加载器)源码篇之rInflate方法

第三部分

我们再看一下第三部分的代码,代码中会有一些简要的说明:

         //》》》》》》》》》》》》》》》》》第三部分》》》》》》》》》》》》》》》》》》》
                    //createViewFromTag是一个根据name来创建View的方法
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    //解析子标签
                    rInflateChildren(parser, temp, attrs, true);

                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            return result;
        }

将第三部分内容分拆一下主要分为以下几块内容:

  • 排除标签为include,或者merge之后,就会通过createViewFromTag()方法来创建View
  • root是inflater()方法的第二个参数,而attachToRoot是第三个参数,最后会根据这两个参数来决定返回的View

在这部分中,createViewFromTag()是根据name(名称),来创建View的一个方法。

接下来,我们要介绍的是inflater()方法中的参数,到底有什么作用?

                    ViewGroup.LayoutParams params = null;
                    //当Root存在
                    if (root != null) {
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            //设置View在父布局下Params
                            temp.setLayoutParams(params);
                        }
                    }
                    //遍历子节点
                    rInflateChildren(parser, temp, attrs, true);

                    //如果Root存在并且attachToRoot为true,即与父View绑定
                    //这里在解析的同时,就会将其添加至父View上
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    //如果父Viewwe为null或者没有绑定父View都会将当前解析的View返回,否则返回父View
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

仔细分析上述代码,可以得出如下结论:

从这段代码中,得出以下几个结论:

  1. 当root为null时,attachToRoot参数无效,而解析出的View作为一个独立的View存在(不存在LayoutParams)。
  2. 当root不为null时,attactToRoot为false,那么会给该View设置一个父View的约束(LayoutParams),然后将其返回。
  3. 当root不为null时,attactToRoot为true,那么该View会被直接addView进父View,然后会将父View返回。
  4. 当root不为null的话,attactToRoot的默认值是true。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
    return inflate(parser, root, root != null);
}

上面的代码中,我们还少分析了一处代码rInflateChildren(),即解析子类:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

如果你之前没看过这段代码,其实你会像博主之前一样,一直在试,而不知道这段代码正确的含义,但是有时候源码会是一个很好的老师,通过它能够得到你想要的。

流程图

20171105150981981211425.png
20171105150981981211425.png

CreateViewFromTag源码解析

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}

createViewFromTag在LayoutInflater中存在重载,最终还是会调用5个参数的createViewFromTag方法。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {

    //解析view标签
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    //如果需要该标签与主题相关,需要对context进行包装,将主题信息加入context包装类ContextWrapper
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
            ta.recycle();
        }

        //BlinkLayout是一种闪烁的FrameLayout,它包裹的内容会一直闪烁,类似QQ提示消息那种。
        if (name.equals(TAG_1995)) {
            return new BlinkLayout(context, attrs);
        }

        //设置Factory,来对View做额外的拓展,这块属于可定制的内容
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            //如果此时不存在Factory,不管Factory还是Factory2,还是mPrivateFactory都不存在,那么会直接对name直接进行解析
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //如果name中包含.即为自定义View,否则为原生的View控件
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

        return view;

根据源码可以将createViewFromTag分为三个流程:

  • 对一些特殊标签,做分别处理,例如:view,TAG_1995(blink)
  • 进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View
  • 如果没有设置Factory或Factory2,那么就会使用LayoutInflater默认的生成方式,进行View的生成

createViewFromTag分析过程:

处理view标签

如果标签的名称是view,注意是小写的view,这个标签一般大家不太常用,具体的使用情况如下:

<view
    class="RelativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"></view>

在使用时,相当于所有控件标签的父类一样,可以设置class属性,这个属性会决定view这个节点会变成什么控件。

如果该节点与主题相关,则需要特殊处理

如果该节点与主题(Theme)相关,需要将context与theme信息包装至ContextWrapper类。

处理TAG_1995标签

这就有意思了,TAG_1995指的是blink这个标签,这个标签感觉使用的很少,以至于大家根本不知道。

这个标签最后会被解析成BlinkLayout,BlinkLayout其实就是一个FrameLayout,这个控件最后会将包裹内容一直闪烁(就和电脑版QQ消息提示一样),有空大家可以自行尝试下,很简单,下面贴一下用法:

<blink
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这个标签会一直闪烁"/>
</blink>

判断其是否存在Factory或者Factory2

在这里先对Factory进行判空,这里不管Factory还是Factory2(mPrivateFactory 就是Factory2),本质上都是一种扩展操作,提前解析name,然后直接将解析后的View返回。

Factory
public interface Factory {
    public View onCreateView(String name, Context context, AttributeSet attrs);
}
Factory2
public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

从这里可以看出,Factory2和Factory都是一个接口,需要自己实现,而Factory2和Factory的区别是Factory2继承Factory,从而扩展出一个参数,就是增加了该节点的父View。

这里我自定义了一个Factory,下面自定义解析View的过程:

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    View view = null;
    try {
        if (-1 == name.lastIndexOf(".")) {
            if (name.equals("View") || name.equals("ViewGroup")) {
                view = mInflater.createView(name, "android.view.", attrs);
            } else {
                view = mInflater.createView(name, "android.widget.", attrs);
            }
        } else {
            if (name.contains(".")) {
                String checkName = name.substring(name.lastIndexOf("."));
                String prefix = name.substring(0, name.lastIndexOf("."));
                view = mInflater.createView(checkName, prefix, attrs);
            }
        }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        if(view != null){
            //在这里可以对View做一些额外的操作,并且能够获得View的属性集,可以做一些自定义操作。
            view.xxxxxx
        }

        return view;
}

从上面可以看出,Factory和Factory2其实LayoutInflater解析View时的一种扩展实现,在这里可以额外的对View处理,设置Factory和Factory2需要通过setFactory()或者setFactory2()来实现。

setFactory()

public void setFactory(Factory factory) {
    //如果已经设置Factory,不可以继续设置Factory
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    //设置Factory会添加一个标记
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

setFactory2()

public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    //注意设置Factory和Factory2的标记是共用的
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

通过上面代码可以看出,Factory和Factory2只能够设置一次,并且Factory和Factory2二者互斥,只能存在一个。

所以一般setFactory()或者setFactory2(),一般在cloneInContext()之后设置,这样生成一个新的LayoutInflater,标记默认是false,才能够设置。

LayoutInflater内置的解析过程

如果Factory或者Factory2没有设置,或者返回View为null,才会使用默认解析方式。

if (-1 == name.indexOf('.')) {
    view = onCreateView(parent, name, attrs);
} else {
    view = createView(name, null, attrs);
}

这段就是对自定义View和原生的控件进行判断,这里给大家说明下原生控件和自定义View的name区别:

原生 :  RelativeLayout
自定义View : com.demo.guidepagedemo.customview.CustomImageView

原生控件的解析方式 onCreateView :

protected View onCreateView(View parent, String name, AttributeSet attrs) throws ClassNotFoundException {
    return onCreateView(name, attrs);
}

然后调用的还是2个参数的onCreateView()方法

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

可以看到最终方法的指向还是调用createView方法:

public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException {
    //判断构造器是否存在    
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    try {
        //如果构造器不存在,这个就相当于Class之前是否被加载过,sConstructorMap就是缓存这些Class的Map
        if (constructor == null) {
            //通过前缀+name的方式去加载
            clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
            //通过过滤去设置一些不需要加载的对象
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            //缓存Class
            sConstructorMap.put(name, constructor);
        } else {
            //如果Class存在,并且加载Class的ClassLoader合法
            //这里先判断该Class是否应该被过滤
            if (mFilter != null) {
                //过滤器也有缓存之前的Class是否被允许加载,判断这个Class的过滤状态
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    //加载Class对象操作
                    clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
                    //判断Class是否可被加载
                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    mFilterMap.put(name, allowed);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;

        //如果过滤器不存在,直接实例化该View
        final View view = constructor.newInstance(args);
        //如果View属于ViewStub那么需要给ViewStub设置一个克隆过的LayoutInflater
        if (view instanceof ViewStub) {
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        return view;
    }
}

上面代码有点长,就直接在代码里面加注释了,这里额外说一下这个方法:

判断ClassLoader是否安全的verifyClassLoader :

private final boolean verifyClassLoader(Constructor<? extends View> constructor) {
    final ClassLoader constructorLoader = constructor.getDeclaringClass().getClassLoader();
    if (constructorLoader == BOOT_CLASS_LOADER) {
        //这里注意BootClassLoader是相当于所有派生出来的ClassLoader的原始基类,所有的ClassLoader都是根据其衍生的。
        return true;
    }
    //这里是一个遍历操作,一直在遍历加载mContext的ClassLoader的继承树,一直在往上寻找,如果
    //constructor的ClassLoader与继承树中某个ClassLoader相同就说明这个ClassLoader是安全的
    ClassLoader cl = mContext.getClassLoader();
    do {
        if (constructorLoader == cl) {
            return true;
        }
        cl = cl.getParent();
    } while (cl != null);
        return false;
    }
}

这里简单说明下,几种ClassLoader的作用:

  • BootClassLoader 加载Android FrameWork层的一些字节码文件
  • PathClassLoader 加载已经安装到系统上的应用App(apk)上的字节码文件
  • DexClassLoader 加载指定目录中的Class字节码文件
  • BaseDexClassLoader 是PathClassloader和DexClassLoader的父类

一般的App刚启动的时候,就会有两个ClassLoader被加载,分别是PathClassLoader、DexClassLoader而这两个ClassLoader都是继承BaseDexClassLoader.

而BaseDexClassLoader继承的是ClassLoader,但是在ClassLoader中getParent()方法赋予其Parent为BootClassLoader,这个如果大家感兴趣,可以自行查阅ClassLoader。

流程图

20171105150982093163000.png
20171105150982093163000.png

rInflate()的源码分析

void rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    //获取该标签的深度
    final int depth = parser.getDepth();
    int type;

    while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        //如果该节点为requestFocus
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
            //如果该节点为tag
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
            //如果该节点为include标签
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            //解析include标签
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            //如果该节点为Merge
            throw new InflateException("<merge /> must be the root element");
        } else {
            //否则属于正常的View
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            //接下来解析子View
            rInflateChildren(parser, view, attrs, true);
            //注意这里直接进行addView操作
            viewGroup.addView(view, params);
        }
    }

    //如果解析完成,需要通知父View,解析完成。
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

在rInflate这里做的操作,就是识别这些节点,然后对应解析形成响应的元素,下面我们会根据代码,一段一段分析rInflate都做了什么.

  • 如果发现requestFocus标签,则调用父View的requestFocus()方法。

requestFocus标签使用

<EditText  
    android:id="@+id/text"  
    android:layout_width="match_parent"  
    android:layout_height="wrap_content" >  
    <!-- 当前控件处于焦点状态 -->  
<requestFocus />  

parseRequestFocus方法

private void parseRequestFocus(XmlPullParser parser, View view) throws XmlPullParserException, IOException {
    //调用其父View的requestFocus()方法
    view.requestFocus();
    consumeChildElements(parser);
}
  • 如果发现tag标签,为其设置(key,value)模式的tag。

tag标签使用

<Button
    android:id="@+id/tag_btn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="openClickNotification"
    android:text="自定义带监听事件的通知">

    <tag
        android:id="@+id/tag_id"
        android:value="@string/app_name" />
</Button>

parseViewTag方法 :

private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs) throws XmlPullParserException, IOException {
    final Context context = view.getContext();
    final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
    //这里设置tag的key
    final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
    //这里设置tag的value
    final CharSequence value = ta.getText(R.styleable.ViewTag_value);
    view.setTag(key, value);
    ta.recycle();
    consumeChildElements(parser);
}

在parseViewTag()方法中,会把(key,value)形式的tag赋予View。

Key指的是R.id.tag_id对应的int类型数据;

Value指的是R.string.app_name的String类型数据;

  • 如果是Include标签,这里开始先获取了Include的深度

final int depth = parser.getDepth();

所谓深度就是XML的层级关系,例如这样:

<!-- outside -->     
<root>                     
   sometext                
   <foobar>                    
    </foobar>                  
</root>                    
<!-- outside -->     

判断该Include标签的深度是否是0,如果为0,则抛出异常,因为include不能为根元素。

  • 如果是Merge标签,那么会直接抛出异常,因为Merge必须为根元素,也就是深度为0的节点。

  • 最后是其他标签,例如View,一起其他的一些标签

final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);

在加载View的过程,大致分为三个阶段:

  • createViewFromTag() 见名知意,根据节点名称创建View
  • rInflateChildren() 加载该节点内子类
  • parent.addView() 最后将该View添加进Parent布局

第一阶段 : createViewFromTag()

createViewFromTag()是根据name(节点名称)来解析出View的一个方法

第二阶段 :rInflateChildren()

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

这里可以看到,这里会将解析出来的View作为Root(父View),继续进行子节点的解析,会继续调用,直到无法解析。

这里的无法解析是指:

  • 当前解析的标识为XmlPullParser.END_TAG(节点结束的标识符),并且深度不在父节点的标签内。
  • 或者type 为 XmlPullParser.END_DOCUMENT(文档结束的标识符)。

第三阶段 parent.addView()将View添加进父View中

viewGroup.addView(view, params);

这段话,不难理解,就是将解析出的View,添加到父View中。

流程图

20171105150982208420747.png
20171105150982208420747.png

parseInclude()是在哪里使用的?

void rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    //----------------省略部分代码--------------------//

} else if (TAG_INCLUDE.equals(name)) {
    if (parser.getDepth() == 0) {
        throw new InflateException("<include /> cannot be the root element");
    }
    parseInclude(parser, context, parent, attrs);
}
//----------------省略部分代码--------------------//
}

从上来代码中,可以发现parseInclude()是在rInflate()中出现,作用是处理当前节点是Include标签时的状况。

parseInclude()源码解析

 //参数说明:
 // parser      解析布局的解析器
 // context     当前加载布局的上下文对象
 // parent      父容器
 // attrs       属性集合(XML该节点的属性集合)
 private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        // 判断 Include标签是否在 ViewGroup容器之内,因为 include 标签只能存在于 ViewGroup 容器之内。

        if (parent instanceof ViewGroup) {

            //------------------<第一部分>-------------------//

            //当开发者设置 include 主题属性时,可以覆盖被 include 包裹View的主题属性。
            //但是这种操作很少会使用。
            //所以如果被包裹 View 设置主题属性,我们在设置就会出现覆盖效果。
            //以 include 标签的主题属性为最终的主题属性

            //提取出 include 的 thme 属性,如果设置了 them 属性,那么include 包裹的View 设置的 theme 将会无效
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();


            //------------------<第二部分>-------------------//

            //如果这个属性是指向主题中的某个属性,我们必须设法得到主题中layout 的资源标识符
            //先获取 layout 属性(资源 id)是否设置
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
            //如果没直接设置布局的资源 id,那么就检索?attr/name这一类的 layout 属性
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                if (value == null || value.length() <= 0) {
                    throw new InflateException("You must specify a layout in the"
                            + " include tag: <include layout=\"@layout/layoutID\" />");
                }

                //从  ?attr/name 这一类的属性中,获取布局属性  
                layout = context.getResources().getIdentifier(value.substring(1), null, null);
            }

            //这个布局资源也许存在主题属性中,所以需要去主题属性中解析
            if (mTempValue == null) {
                mTempValue = new TypedValue();
            }
            if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
                layout = mTempValue.resourceId;
            }


            //------------------<第三部分>-------------------//

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {
                final XmlResourceParser childParser = context.getResources().getLayout(layout);

                try {
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty.
                    }

                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }

                    final String childName = childParser.getName();

                    if (TAG_MERGE.equals(childName)) {
                        //解析 Meger 标签
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        //根据 name名称来创建View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;


                        //获取 View 的 id 和其 Visiable 属性
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();

                        //需要将 Parent中的 LayoutParams 设置为其 Params 属性。
                        //如果 Parent 没有通用的 Params,那么就会抛出Runtime 异常

                        //然后会为其设置 include 包裹内容的通用 Params,

                        ViewGroup.LayoutParams params = null;
                        try {
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

                        // 解析子标签
                        rInflateChildren(childParser, view, childAttrs, true);

                        if (id != View.NO_ID) {
                            view.setId(id);
                        }

                        // 加载include内容时,需要直接设置其 可见性
                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        //添加至父容器中
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

        LayoutInflater.consumeChildElements(parser);
    }

先把parseInclude()这个方法全景先看下,然后我们在进行分拆,一部分一部分分析。

parseInclude()参数解读

parseInclude()中分别含义四个参数:

解析器 -> XmlPullParser parser

用来解析XML文件的解析器,通过解析器可以得到当前节点的相对应的AttributeSet(属性集)

上下文对象 - > Context context

当前加载该XML的上下文对象,并且这个Context与LayoutInflater属于相互绑定关系(一一对应)

父容器 - > View parent

包裹该节点的父容器,一般来说都是继承ViewGroup实现的视图组

属性集 -> AttributeSet attrs

该节点的属性集,包括所有该节点的相关属性

Include中的theme属性

这里大家先了解一个相关的问题,关于include标签设置theme属性的情况:

一般来说theme(主题)一般出现在Activtiy的AndroidManifest文件下,来给Activity设置统一的布局效果,而且可以使用如下的操作来进行主题属性的使用。

//  ?attr这样的形式,使用主题中的设置参数
android:background="?attr/colorPrimary"

如果Include标签下设置了新的theme,那么Include中的内容在使用主题属性时,使用的theme主题就是(include)设置的内容,而不是Activity默认下的主题,形成了一种覆盖效果。

也就是说Include标签设置的主题可以覆盖Activity设置的根主题,但是Include设置的主题只作用与Include内部。

举个栗子:

style.xml

先定义好两个基础Theme,一个是作为App的基础主题,另一个是include中的主题。

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- BaseApplication theme -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>


<style name="IncludeTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Include Theme -->
    <item name="colorPrimary">@color/colorAccent</item>
    <item name="colorPrimaryDark">@color/colorAccent</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

AndroidManifest.xml

设置Activity的基础主题为AppTheme

<activity
    android:name="com.demo.MainActivity"
    android:theme="@style/AppTheme"></activity>

activity_main.xml

<?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"
    android:orientation="vertical">

    <!-- 这里是使用基础Theme的Toolbar -->
    <android.support.v7.widget.Toolbar
        android:id="@+id/activity_theme_tb"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?attr/colorPrimary" />

    <!-- 这里是自带Theme Include的Toolbar -->
    <include
        layout="@layout/test_toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:theme="@style/IncludeTheme" />

</RelativeLayout>

接下来,我们在看一下Include包裹的布局

test_toolbar.xml

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/include_toolbar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?attr/colorPrimary" />

</LinearLayout>

从上面的XML文件我们可以看出两个Toolbar调用的background都指向theme的colorPrimary属性,接下来看一下显示效果:

20171105150982270724112.png
20171105150982270724112.png

从效果图可以发现,Include Toolbar显示的颜色是粉色的,也就是Include额外设置的theme,这里也是从正面证明了这个概念。

第一部分:Include Theme主题的设置

//------------------<第一部分>-------------------//
//提取出Theme属性
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
//如果存在Theme属性,那么Include包含的子标签都会使用该主题
if (hasThemeOverride) {
    context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();

通过上面的介绍,很明显这段代码含义,就是检测是否给Include标签设置了Theme属性,如果设置theme,就创建相应的ContextThemeWrapper,用于之后子标签的解析时theme的使用。

第二部分:Include 内容布局的设置

//------------------<第二部分>-------------------//
//先获取 layout 属性(资源 id)是否设置
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
    //如果没直接设置布局的资源 id,那么就检索?attr/name这一类的 layout 属性
    final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
    if (value == null || value.length() <= 0) {
        throw new InflateException("You must specify a layout in the" + " include tag: <include layout=\"@layout/layoutID\" />");
    }

    //从?attr/name 这一类的属性中,获取布局属性  
    layout = context.getResources().getIdentifier(value.substring(1), null, null);
}

//这个布局资源也许存在主题属性中,所以需要去主题属性中解析
if (mTempValue == null) {
    mTempValue = new TypedValue();
}
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
    layout = mTempValue.resourceId;
}

这部分的内容主要是提取Include的内容布局的提取,Include的内容布局的设置有两种:

第一种 : 直接@layout 后面设置布局的XML

layout="@layout/test_toolbar"

第二种:通过引入theme的item设置的layout属性

Include标签下:

layout="?attr/theme_layout"

包裹Include标签的布局Theme(注意:这里不是Include设置的主题):

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    //重点在这里!!!!!
    <item name="theme_layout">@layout/test_toolbar</item>
</style>

而上面的代码的作用是检索layout属性,如果layout已经以第一种方式引入,就不需要在去theme中检索,如果layout第一种方式检索不到资源ID,那么就会去以第二种方式进行检索。

第三部分: Include标签的View处理

            //------------------<第三部分>-------------------//
            //如果此时还找不到layout,那么必然异常~,会报找不到资源ID的layout异常
            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {
            //生成子解析器
                final XmlResourceParser childParser = context.getResources().getLayout(layout);

                try {
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                    //----------------省略了XML一些规则的判断----------------//
                    //获取子节点的名称
                    final String childName = childParser.getName();
                    if (TAG_MERGE.equals(childName)) {
                        //解析 Meger 标签
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        //根据 name名称来创建View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;
                        //获取 View 的 id 和其 Visiable 属性
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();

                        //需要将 Parent中的 LayoutParams 设置为其 Params 属性。
                        //如果 Parent 没有通用的 Params,那么就会抛出Runtime 异常

                        //然后会为其设置 include 包裹内容的通用 Params,

                        ViewGroup.LayoutParams params = null;
                        try {
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

                        // 解析子标签
                        rInflateChildren(childParser, view, childAttrs, true);

                        if (id != View.NO_ID) {
                            view.setId(id);
                        }

                        // 加载include内容时,需要直接设置其 可见性
                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        //添加至父容器中
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

这部分主要的作用是解析Include包裹layout的根标签:

(1)先特别处理Merge标签 :

如果子节点是Merge标签,那么直接进行内容的解析,调用rInflater()方法。

而rInflater()这个方法的作用是,解析某个节点,根据节点的不同类型从而进行不同的处理

(2)解析Include的内容:

在这之前先通过createViewFromTag()方法,根据名称来生成相对应的View

这里分成两块内容

第一块是设置LayoutParams

ViewGroup.LayoutParams params = null;
try {
    //加载Include的父ViewGroup的LayoutParams
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
    // Ignore, just fail over to child attrs.
}
if (params == null) {
    //加载Include的子ViewGroup的LayoutParams
    params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);

这段的作用是为Include的包裹的根View设置LayoutParams,使用的LayoutParams默认是Include外层的ViewGroup。

如果此时Params加载失败,那就会使用Include包裹的ViewGroup的LayoutParams,反正怎么都得设置一个。

第二块是在这里设置子ViewGroup的显隐性

// 加载include内容时,需要直接设置其 可见性
switch (visibility) {
    case 0:
        view.setVisibility(View.VISIBLE);
        break;
    case 1:
        view.setVisibility(View.INVISIBLE);
        break;
    case 2:
        view.setVisibility(View.GONE);
        break;
}
//添加至父容器中
group.addView(view);

设置ViewGroup的显隐性,之后就将其添加至父View中,至此parseInclude的分析就到此结束。

流程图

20171105150982400667682.png
20171105150982400667682.png

效果

201711041509789321695.jpg
201711041509789321695.jpg

20171105150982283383132.gif
20171105150982283383132.gif

分析

这个效果属于视觉差的效果,原理是根据ViewPager的滑动方向,页面内物理做同向偏移,只要偏移距离大于页面的偏移,就会产生速度差,那么就会实现该效果。

实现速度差,我们需要一个滑动的比例系数:

在页面进入时:

页面物体的移动距离 = (页面长度 - 滑动距离) * 滑动系数

在页面滑出时:

页面物体的移动距离 = (0 - 滑动距离 ) * 滑动系数

同时考虑第二张Gif上,发现物体Y轴也存在移动,所以也得需要考虑Y轴方向的滑动,整理下:

//进入时:
view.setTranslateX((vpWidth - positionOffsetPixels) * xIn);
view.setTranslateY((vpWidth - positionOffsetPixels) * yIn);

//退出时
view.setTranslateX((0 - positionOffsetPixels) * xOut);
view.setTranslateY((0 - positionOffsetPixels) * yOut);

这样就可以实现出:

  • 进入该界面时,界面上的物品快速飞进来。
  • 退出该界面时,界面上的物理快速飞出去。

实现思路

对于上述的分析,这里的实现思路存在两种:

  1. 自定义View,自定义xIn、yIn、xOut、yOut四个属性的系数,所有界面上的物体继承这个自定义View。
  2. 自定义LayoutInflater.Factory在解析时,将这些自定义属性提取,以Tag方式储存起来。

优缺点分析

自定义View

优点:可以对物体做更多层面的扩展,这个自定义LayoutInflater.Factory是不具备的。

缺点:由于界面的物体数量过多,在findViewById时需要处理的View元素过多,极大的增加代码量。

自定义LayoutInflater.Factory :

优点:可以在解析过程中对View做统一操作,当出现大量的View时,能够缩减大量代码。

缺点:在解析时预处理View,但是就不能动态的改变View的属性,要对View进行扩展性操作,自定义LayoutInflater.Factory不具备这样的功能。

自定义LayoutInflater.Factory

上述的两种方案的优缺点已经分析完毕,但是本文作为实战篇,所以只会介绍自定义LayoutInflater.Factory这种方式。

在实际场景中,需要结合自身情况,以及上述的优缺点,进行合理选择。

在介绍之前,先看一段代码:

View view;
//如果Factory2存在,就会调用其onCreateView方法
if (mFactory2 != null) {
    view = mFactory2.onCreateView(parent, name, context, attrs);
    //如果Factory存在,就会调用其onCreateView方法,和Factory2不同的时,这里的参数没有父View
} else if (mFactory != null) {
    view = mFactory.onCreateView(name, context, attrs);
} else {
    view = null;
}
//如果没有Factory或者Factory2,就会寻找mPrivateFactory(本质上也是Factory2)
if (view == null && mPrivateFactory != null) {
    view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

这段代码出自LayoutInflater中createViewFromTag()方法,作用是根据View的名称(name参数)来创建View.

在这里就简单描述下,这个方法的主要流程:

  • 对一些特殊标签,做分别处理,例如:view,TAG_1995(blink)
  • 进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View
  • 如果没有设置Factory或Factory2,那么就会使用LayoutInflater默认的生成方式,进行View的生成

在实战篇中,只有第二部分和我们今天的内容是相关的,我们在看一遍第二条。

进行对Factory、Factory2的设置判断,如果设置那么就会通过设置Factory、Factory2进行生成View

如果设置了Factory或者Factory2,那么就不会使用LayoutInflater默认的生成方式,那么生成View的过程,就由我们自主把控,这才是我们自定义LayoutInflater.Factory的主要原因。

自定义Factory还是Factory2 ?

View view;
//如果Factory2存在,就会调用其onCreateView方法
if (mFactory2 != null) {
    view = mFactory2.onCreateView(parent, name, context, attrs);
    //如果Factory存在,就会调用其onCreateView方法,和Factory2不同的时,这里的参数没有父View
} else if (mFactory != null) {
    view = mFactory.onCreateView(name, context, attrs);
} else {
    view = null;
}

我们能够从这段代码中得出,Factory2比Factory的优先级要高,即Factory2存在Factory就不可能会被调用,同理可以得出结论:

优先级顺序:

mFactory2  > mFactory > mPrivateFactory > LayoutInflater默认处理方式

而且我们还能够发现mFactory2的onCreateView()方法与mFactory是不相同的:

//mFactory2
mFactory2.onCreateView(parent, name, context, attrs);

//mFactory
view = mFactory.onCreateView(name, context, attrs);

根据上述的分析,我们可以得出结论:

(1)Factory2的调用优先级比Factory要高

(2)Factory2的onCreateView()方法,会比Factory多返回一个父View的参数。

(3)Factory2和Factory是互斥的,(如果不通过反射的话)只能设置一个。

第三条在CreateViewFromTag的那篇文章已经分析过了,这里不做过多的解释了。

实际选择的过程中,一般会选择自定义Factory2,因为Factory2本身也继承了Factory接口,而且Factory2的优先级比较高。

注意事项

设置Factory但是发现无响应,是因为本身LayoutInflater中存在Factory2**

因为一般使用方式,是直接调用cloneInContext()方法,我们知道一般的默认解析器都是PhoneLayoutInflater,我们看下其实现方式:

protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
    super(original, newContext);
}

本质就是调用LayoutInflater的两参构造方法:

protected LayoutInflater(LayoutInflater original, Context newContext) {
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
}

在这里可以看出,cloneInContext会把原LayoutInflater的Factory2和Factory一并复制。

因为Factory比Factory2的优先级低,所以才会不出现效果。

解决方案

(1)自定义LayoutInflater,并且改写cloneInContext,使其不复制原LayoutInflater的Factory2以及Factory。

public class CustomLayoutInflater extends LayoutInflater {

    protected CustomLayoutInflater(Context context) {
        super(context);
    }

    @Override
    public LayoutInflater cloneInContext(Context newContext) {
        return new CustomLayoutInflater(newContext);
    }
}

(2)使用时,直接通过new出实例,然后setFactory

CustomLayoutInflater newInflater = new CustomLayoutInflater(getActivity());
newInflater.setFactory2(new CustomAppFactory(newInflater, this));
return newInflater.inflate(layoutId, null);

使用AppCompatActivity直接setFactory2或者setFactory为什么报错?

这是因为 AppCompatActivity 在初始化的时候,已经设置了 Factory,下面来看下这部分代码

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    //注意这个方法
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    //.....省略多余的代码..........
    super.onCreate(savedInstanceState);
}

继续查看 installViewFactory()方法

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        //这句话是设置 Factory 的方法
        LayoutInflaterCompat.setFactory(layoutInflater, this);
    } else {
        //省略部分代码。。。。。。      
    }
}

可以发现,在onCreate 时 LayoutInflater 已经设置过一次 Factory 了,然后我再来看下 setFactory() 的源码:

public void setFactory(Factory factory) {
    if (mFactorySet) {
        //原因就是这一句
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

根据上面代码,就可以发现报错原因了。

解决方案

在使用前,先使用 cloneInContext()克隆出一个新的 LayoutInflater,然后在进行设置操作。

LayoutInflate  newInflater = LayoutInflater.cloneInContext(inflater,context);

newInflater.setFactory(new CustomFactory());

这样就避开在原 LayoutInflater 设置 Factory 报错了。

自定义Factory2的实现 ——> CustomAppFactory

根据上面的展示效果,我们可以判断出是ViewPager + Fragment的风格,所以我们自定义Factory应该在Fragment的onCreateView中,更改LayoutInflater。

而且根据注意事项,我们一般会自定义优先级较高的Factory2,防止本身cloneInContext的LayoutInflater中已经存在Factory2,我们使用Factory会无效。

使用方式

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    Bundle bundle = getArguments();
    int layoutId = bundle.getInt(LAYOUT_ID);
    //注意需要调用cloneInContext方法生成新的LayoutInflater
    LayoutInflater newInflater = inflater.cloneInContext(getActivity());
    //调用的是setFactory2而非setFactory
    newInflater.setFactory2(new CustomAppFactory(newInflater, this));
    return newInflater.inflate(layoutId, null);
}

自定义过程

那么就创建一个类CustomAppFactory来实现Factory2的接口,复写onCreateView的方法。

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    View view = null;
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<第一部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    try {
        if (name.contains(".")) {
            String checkName = name.substring(name.lastIndexOf("."));
            String prefix = name.substring(0, name.lastIndexOf("."));
            view = defaultInflater(checkName, prefix, attrs);
        }
        if (name.equals("View") || name.equals("ViewGroup")) {
            view = defaultInflater(name, sClassPrefix[1], attrs);
        } else {
            view = defaultInflater(name, sClassPrefix[0], attrs);
        }
        //<<<<<<<<<<<<<<<<<<<<<<<<<<<第二部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        //实例化完成
        if (view != null) {
            //获取自定义属性,通过标签关联到视图上
            setViewTag(view, context, attrs);
            mInflaterView.addView(view);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return view;
}

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    View view = onCreateView(name, context, attrs);
    return view;
}

其实如果我们采取自定义的方式,这里只会调用onCreateView()四位参数的方法,因为在比较Factory2和Factory的代码也介绍过了。

我们实现的逻辑是在onCreateView()三位逻辑里面,因为需要实现的效果不需要Parent(父View),所以这里逻辑实现全在三位参数的onCreateView()中。

在这里我们将onCreateView()中,分成2部分内容:

  • 根据名称解析出View
  • 扩展操作,将额外的属性,提取出来储存在Tag中

onCreateView第一部分内容

if (name.contains(".")) {
    String checkName = name.substring(name.lastIndexOf("."));
    String prefix = name.substring(0, name.lastIndexOf("."));
    view = defaultInflater(checkName, prefix, attrs);
}
if (name.equals("View") || name.equals("ViewGroup")) {
    view = defaultInflater(name, sClassPrefix[1], attrs);
} else {
    view = defaultInflater(name, sClassPrefix[0], attrs);
}

这里判断了name中是否包含“.”,是用来判断生成的View是否是自定义View,下面来看下自定义View和Android自带的组件的区别:

//原生的组件
RelativeLayout
//自定义View
com.demo.guidepagedemo.customview.CustomImageView

可以发现区别为原生的View不带前缀,而自定义View是包括前缀的,所以会用name.contains(“.”)来区分。

而原生组件中View和ViewGroup是属于android.view包下,其他的例如:RelativeLayout,LinearLayout是属于android.widget包下。

private final String[] sClassPrefix = {
    "android.widget.",
    "android.view."
};

所以在之后会对View和ViewGroup作区分,上面把sClassPrefix贴出来了。

而这里真正的解析过程最后还是交给LayoutInflater,调用LayoutInflater的onCreateView方法:

private View defaultInflater(String name, String prefix, AttributeSet attrs) {
    View view = null;
    try {
        view = mInflater.createView(name, prefix, attrs);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return view;
}

onCreateView第二部分内容

//实例化完成
if (view != null) {
    //获取自定义属性,通过标签关联到视图上
    setViewTag(view, context, attrs);
    mInflaterView.addView(view);
}

在这里做拓展处理的,setViewTag方法是处理View的自定义属性,然后将这些属性包装成类,给View设置Tag

setViewTag方法

/**
 * 将View的属性信息存储在Tag中
 */
private void setViewTag(View view, Context context, AttributeSet attrs) {
    //解析自定义的属性
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomImageView);
    if (attrs != null && array.length() > 0) {
        AttrTagBean bean = new AttrTagBean();
        bean.xIn = array.getFloat(R.styleable.CustomImageView_in_value_x, 0f);
        bean.xOut = array.getFloat(R.styleable.CustomImageView_out_value_x, 0f);
        bean.yIn = array.getFloat(R.styleable.CustomImageView_in_value_y, 0f);
        bean.yOut = array.getFloat(R.styleable.CustomImageView_out_value_y, 0f);
        //index
        view.setTag(bean);
    }
    array.recycle();
}

上面对应的是本文我们开始设置的4个系数:

R.styleable.CustomImageView_in_value_x              -->   进入时 x方向的系数

R.styleable.CustomImageView_out_value_x             -->   退出时 x方向的系数

R.styleable.CustomImageView_in_value_y              -->   进入时 y方向的系数

R.styleable.CustomImageView_out_value_y             -->   退出时 y方向的系数

而这里的mInflaterView是一个抽象接口,让Fragment来实现的,通过在Fragment中内置一个List《View》,到时候可以遍历统一操作这些View,下面是实现过程:

public interface InflaterViewImpl {

    /**
     * 获取View集合
     *
     * @return
     */
    List<View> getViews();


    /**
     * 添加元素
     */
    void addView(View view);
}

Fragment中的实现过程

public class PageFragment extends Fragment implements InflaterViewImpl {

    private List<View> views = new ArrayList<>();

    //**************篇幅原因省略了部分方法************************//

    @Override
    public List<View> getViews() {
        return views;
    }

    @Override
    public void addView(View view) {
        if (views.contains(view)) {
            return;
        }
        views.add(view);
    }
}

处理ViewPager的滑动

这是实战篇的最后一部分内容,主要介绍的是ViewPager的滑动监听相关的处理,因为所有效果是基于ViewPager的滑动监听来显示的。

因为本文主要介绍内容是自定义LayoutInflater.Factory,所以这里会简单叙述下:

mInflaterVp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        //获取ViewPager的宽度
        int vpWidth = mInflaterVp.getWidth();
        //获取正在进入的界面
        PageFragment inFragment = getPosition(position - 1);
        if (inFragment != null) {
            List<View> views = inFragment.getViews();
            if (views != null && views.size() > 0) {
            for (View view : views) {
                AttrTagBean tag = (AttrTagBean) view.getTag();
                if (tag != null) {
                    view.setTranslationX((vpWidth - positionOffsetPixels) * tag.xIn);
                    view.setTranslationY((vpWidth - positionOffsetPixels) * tag.yIn);
                }
            }
        }
    }

    //当前正在滑动的界面
    PageFragment outFragment = getPosition(position);
    if (outFragment != null) {
        List<View> views = outFragment.getViews();
        if (views != null && views.size() > 0) {
            for (View view : views) {
                AttrTagBean tag = (AttrTagBean) view.getTag();
                if (tag != null) {
                    view.setTranslationX((0 - positionOffsetPixels) * tag.xOut);
                    view.setTranslationY((0 - positionOffsetPixels) * tag.yOut);
                }
            }
        }
    }


    @Override
    public void onPageSelected(int position) {
        //当划到最后一页时,小人的图标消失
        if (position == fragments.size() - 1) {
            mInflaterIv.setVisibility(View.GONE);
        } else {
            mInflaterIv.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        //这里是处理图中的小人的帧动画过程
        Drawable anim = mInflaterIv.getBackground();
        if (!(anim instanceof AnimationDrawable)) {
            return;
        }
        AnimationDrawable animation = (AnimationDrawable) anim;
        Log.d("滑动状态", state + "");
        switch (state) {
            //空闲状态
            case ViewPager.SCROLL_STATE_IDLE:
                animation.stop();
                break;
            //拖动状态
            case ViewPager.SCROLL_STATE_DRAGGING:
                animation.start();
                break;
            //惯性滑动状态
            case ViewPager.SCROLL_STATE_SETTLING:
                break;
        }
    }
});

小红书引导页

![20171105150982288981203.gif](http://ohtrrgyyd.bkt.clouddn.com/20171105150982288981203.gif)
2017/10/14 posted in  Android

Android-Camera和Matrix实现真正的3D(WheelView)日期,地址选择滚轮控件

前言

Camera和Matrix实现真正的3D(WheelView)日期,地址选择滚轮控件

先看效果图

2017110515098153104205.gif
2017110515098153104205.gif

垂直方向的3D旋转

2017110515098153351203.gif
2017110515098153351203.gif

水平方向的3D旋转

功能分析

3D旋转效果

WheelView的实现方式已经有很多种方式, 而且网上也有实现好的旋转效果,不过只是2D的旋转,而且要处理滑动与单击item事件比较复杂,真正的旋转是要通过Matrix, Camera类来实现,这里的Camera不是照相机里的API,Camera可以实现x,y,z轴的旋转,不清楚的可以去也解这些API的使用, 这里不详细介绍, 配合RecyclerView.ItemDecoration,在每个item中将Canvas进行3D旋转并平移,产生3D视觉效果

这里拿垂直布局的一种状态来做示例

    /**
     * 画垂直布局时的item
     * @param c
     * @param rect
     * @param position
     * @param parentCenterX RecyclerView的中心X点
     * @param parentCenterY RecyclerView的中心Y点
     */
    void drawVerticalItem(Canvas c, Rect rect, int position, float parentCenterX, float parentCenterY) {
        int realPosition = position - itemCount;//数据中的实际位置
        float itemCenterY = rect.exactCenterY();
        float scrollOffY = itemCenterY - parentCenterY;
        float rotateDegreeX = scrollOffY * itemDegree / itemSize;//垂直布局时要以X轴为中心旋转
        int alpha = degreeAlpha(rotateDegreeX);
        if (alpha <= 0) return;
        float rotateSinX = (float) Math.sin(Math.toRadians(rotateDegreeX));
        float rotateOffY = scrollOffY - wheelRadio * rotateSinX;//因旋转导致界面视角的偏移
        //Log.i("you", "drawVerticalItem degree " + rotateDegreeX);
        //计算中心item, 优先最靠近中心区域的为中心点
        boolean isCenterItem = false;
        if (!hasCenterItem) {
            isCenterItem = Math.abs(scrollOffY) <= halfItemHeight;
            if (isCenterItem) {
                centerItemPosition = realPosition;
                hasCenterItem = true;
            }
        }
        //这里是旋转操作的核心,每个item在旋转成弧时,都要将item的中心在旋转后给人的视觉上的偏移计算好
        c.save();
        c.translate(0.0f, -rotateOffY);
        camera.save();
        camera.rotateX(-rotateDegreeX);
        camera.getMatrix(matrix);
        camera.restore();
        matrix.preTranslate(-parentCenterX, -itemCenterY);
        matrix.postTranslate(parentCenterX, itemCenterY);
        c.concat(matrix);
        drawItem(c, rect, realPosition, alpha, isCenterItem, true);
        c.restore();
    }

到这里基本已经实现了每个item距离中心点的旋转效果,接下来就是添加WheelView显示的数量在RecyclerView头与尾部的空的item

适配器定义

滑动的时候,item要能滑动距中心点以上,也可以滑动到中心点以下,所以适配器中的item数量也要对应改变,直接上代码

class WheelViewAdapter extends RecyclerView.Adapter<WheelViewHolder> {

    ...伪代码

    @Override
    public void onBindViewHolder(WheelViewHolder holder, int position) {
    //由于里面的文本全是画的,这里只是绑定最原始的View
    }

    @Override
    public int getItemCount() {
      //  这里的totalItemCount就是滑轮控件距离中心点显示的item个数 乘2
        return totalItemCount + (adapter == null ? 0 : adapter.getItemCount());
    }

    @Override
    public WheelViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //适配器里根据垂直或水平布局显示
        View view = new View(parent.getContext());
        view.setLayoutParams(WheelUtils.createLayoutParams(orientation, itemSize));
        return new WheelViewHolder(view);
    }
}

总结:

WheelView具体使用方法,示例代码中都有详细介绍,由于工作忙没有时间详细介绍里面的内容,源码里都有适当的注释,也可以一起讨论更佳的效果

后面有空再加上item点击与左右偏移时的立体效果,还有封装日期选择等...

2017/10/12 posted in  Android

Android-实现dialog的3D翻转

效果图

本文实现了 Android 中 dialog 的 3D 翻转效果。这里通过一个简单的应用场景记录下。

20171105150981489051639.gif
20171105150981489051639.gif

分析

起初自己的思路是 Activity 进行界面跳转实现旋转效果,网上看了很多,写下来发现效果不对。之后又看到 Google 上面的 Card Flid Animation 效果是这样的。

201711051509814926496.gif
201711051509814926496.gif

看着确实不错,然而拿下来 demo 放慢翻转速度后发现,不是我想要的。但是跟我看到的一个 app 里面的效果一样
然后想改成 dialog 试试效果,发现更是不行了。

Card Flid Animation效果如下:

这个是通过Activity来切换Fragment实现的,可以看到区别是翻转时候貌似会变大,其实没用,只是翻转后的视觉问题。

20171105150981496621515.gif
20171105150981496621515.gif

听说 openGl 比较麻烦,并且没有用过。然后就搜了下Rotate3DAnimaitons。
搜到了
这篇文章
所以这篇文章里的实现方法不是我的原创,是参考人家的。在这里感谢这位大神。
不过他这个是 activity 里的,我就想要一个 dialog 效果,因为电脑上TIM 的打开红包这个 3D 效果看着不错,其实大同小异,就拿过来改成Dialog。
对于 Rotate3DAnimaitons 这篇文章已经很详细了,有需要的可以参考下。

Rotate3dAnimation

/**
 * An animation that rotates the view on the Y axis between two specified angles.
 * This animation also adds a translation on the Z axis (depth) to improve the effect.
 */
public class Rotate3dAnimation extends Animation {
    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mCenterX;
    private final float mCenterY;
    private final float mDepthZ;
    private final boolean mReverse;
    private Camera mCamera;

    /**
     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
     * start angle and its end angle. Both angles are in degrees. The rotation
     * is performed around a center point on the 2D space, definied by a pair
     * of X and Y coordinates, called centerX and centerY. When the animation
     * starts, a translation on the Z axis (depth) is performed. The length
     * of the translation can be specified, as well as whether the translation
     * should be reversed in time.
     *
     * @param fromDegrees the start angle of the 3D rotation //起始角度
     * @param toDegrees the end angle of the 3D rotation //结束角度
     * @param centerX the X center of the 3D rotation //x中轴线
     * @param centerY the Y center of the 3D rotation //y中轴线
     * @param reverse true if the translation should be reversed, false otherwise//是否反转
     */
    public Rotate3dAnimation(float fromDegrees, float toDegrees,
            float centerX, float centerY, float depthZ, boolean reverse) {
        mFromDegrees = fromDegrees;
        mToDegrees = toDegrees;
        mCenterX = centerX;
        mCenterY = centerY;
        mDepthZ = depthZ;//Z轴移动的距离,这个来影响视觉效果,可以解决flip animation那个给人看似放大的效果
        mReverse = reverse;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mCamera = new Camera();
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float fromDegrees = mFromDegrees;
        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

        final float centerX = mCenterX;
        final float centerY = mCenterY;
        final Camera camera = mCamera;

        final Matrix matrix = t.getMatrix();

        Log.i("interpolatedTime", interpolatedTime+"");
        camera.save();
        if (mReverse) {
            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
        } else {
            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
        }
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

dialog实现3D翻转代码

public class MyDialog extends Dialog {

    @BindView(R.id.et_user_name)
    EditText etUserName;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.cb_auto_login)
    CheckBox cbAutoLogin;
    @BindView(R.id.tv_forget_pwd)
    TextView tvForgetPwd;
    @BindView(R.id.ll_content)
    LinearLayout llContent;
    @BindView(R.id.et_email)
    EditText etEmail;
    @BindView(R.id.btn_back)
    Button btnBack;
    @BindView(R.id.container)
    RelativeLayout container;
    private Context context;

    @BindView(R.id.ll_register)
    LinearLayout llRegister;

    //接口回调传递参数
    private OnClickListenerInterface mListener;
    private View view;
//
    private String strContent;

    private int centerX;
    private int centerY;
    private int depthZ = 700;//修改此处可以改变距离来达到你满意的效果
    private int duration = 300;//动画时间
    private Rotate3dAnimation openAnimation;
    private Rotate3dAnimation closeAnimation;

    private boolean isOpen = false;

    public interface OnClickListenerInterface {

        /**
         * 确认,
         */
        void doConfirm();

        /**
         * 取消
         */
//        public void doCancel();
    }

    public MyDialog(Context context) {
        super(context);
        this.context = context;
    }

    public MyDialog(Context context, String content) {
        super(context);
        this.context = context;
        this.strContent = content;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //去掉系统的黑色矩形边框
        getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        init();
    }

    public void init() {
        LayoutInflater inflater = LayoutInflater.from(context);
        view = inflater.inflate(R.layout.dialog_my, null);
        setContentView(view);
        ButterKnife.bind(this);
        etPassword.setTypeface(Typeface.DEFAULT);
        etPassword.setTransformationMethod(new PasswordTransformationMethod());
        tvForgetPwd.setOnClickListener(new OnWidgetClickListener());
        btnBack.setOnClickListener(new OnWidgetClickListener());
        Window dialogWindow = getWindow();
        WindowManager.LayoutParams lp = dialogWindow.getAttributes();
        DisplayMetrics d = context.getResources().getDisplayMetrics(); // 获取屏幕宽、高用
        lp.width = (int) (d.widthPixels * 0.8); // 宽度设置为屏幕的0.8
        lp.height = (int) (d.heightPixels * 0.6); // 高度设置为屏幕的0.6
        dialogWindow.setAttributes(lp);
        setCanceledOnTouchOutside(false);
        setCancelable(true);
    }

    public void setClicklistener(OnClickListenerInterface clickListenerInterface) {
        this.mListener = clickListenerInterface;
    }

    private class OnWidgetClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {

            int id = v.getId();
            switch (id) {
                case R.id.tv_forget_pwd:
                    startAnimation();
                    break;
                case R.id.btn_back:
                    startAnimation();
                    break;
            }
        }
    }

    private void startAnimation() {
        //接口回调传递参数
        centerX = container.getWidth() / 2;
        centerY = container.getHeight() / 2;
        if (openAnimation == null) {
            initOpenAnim();
            initCloseAnim();
        }

        //用作判断当前点击事件发生时动画是否正在执行
        if (openAnimation.hasStarted() && !openAnimation.hasEnded()) {
            return;
        }
        if (closeAnimation.hasStarted() && !closeAnimation.hasEnded()) {
            return;
        }

        //判断动画执行
        if (isOpen) {

            container.startAnimation(openAnimation);

        } else {

            container.startAnimation(closeAnimation);

        }
        isOpen = !isOpen;
    }

    /**
     *注意旋转角度
     */
    private void initOpenAnim() {
        //从0到90度,顺时针旋转视图,此时reverse参数为true,达到90度时动画结束时视图变得不可见,
        openAnimation = new Rotate3dAnimation(0, 90, centerX, centerY, depthZ, true);
        openAnimation.setDuration(duration);
        openAnimation.setFillAfter(true);
        openAnimation.setInterpolator(new AccelerateInterpolator());
        openAnimation.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                llRegister.setVisibility(View.GONE);
                llContent.setVisibility(View.VISIBLE);
                //从270到360度,顺时针旋转视图,此时reverse参数为false,达到360度动画结束时视图变得可见
                Rotate3dAnimation rotateAnimation = new Rotate3dAnimation(270, 360, centerX, centerY, depthZ, false);
                rotateAnimation.setDuration(duration);
                rotateAnimation.setFillAfter(true);
                rotateAnimation.setInterpolator(new DecelerateInterpolator());
                container.startAnimation(rotateAnimation);
            }
        });
    }

    private void initCloseAnim() {
        closeAnimation = new Rotate3dAnimation(360, 270, centerX, centerY, depthZ, true);
        closeAnimation.setDuration(duration);
        closeAnimation.setFillAfter(true);
        closeAnimation.setInterpolator(new AccelerateInterpolator());
        closeAnimation.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                llRegister.setVisibility(View.VISIBLE);
                llContent.setVisibility(View.GONE);
                Rotate3dAnimation rotateAnimation = new Rotate3dAnimation(90, 0, centerX, centerY, depthZ, false);
                rotateAnimation.setDuration(duration);
                rotateAnimation.setFillAfter(true);
                rotateAnimation.setInterpolator(new DecelerateInterpolator());
                container.startAnimation(rotateAnimation);
            }
        });
    }
}
2017/10/11 posted in  Android

Android-SpannableString与SpannableStringBuilder

概述

SpannableString、SpannableStringBuilder与String的关系

首先SpannableString、SpannableStringBuilder基本上与String差不多,也是用来存储字符串,但它们俩的特殊就在于有一个SetSpan()函数,能给这些存储的String添加各种格式或者称样式(Span),将原来的String以不同的样式显示出来,比如在原来String上加下划线、加背景色、改变字体颜色、用图片把指定的文字给替换掉,等等。所以,总而言之,SpannableString、SpannableStringBuilder与String一样, 首先也是传字符串,但SpannableString、SpannableStringBuilder可以对这些字符串添加额外的样式信息,但String则不行。

注意:如果这些额外信息能被所用的方式支持,比如将SpannableString传给TextView;也有对这些额外信息不支持的,比如前一章讲到的Canvas绘制文字,对于不支持的情况,SpannableString和SpannableStringBuilder就是退化为String类型,直接显示原来的String字符串,而不会再显示这些附加的额外信息。

SpannableString与SpannableStringBuilder区别

它们的区别在于 SpannableString像一个String一样,构造对象的时候传入一个String,之后再无法更改String的内容,也无法拼接多个 SpannableString;而SpannableStringBuilder则更像是StringBuilder,它可以通过其append()方法来拼接多个String:

//使用SpannableString,必须一次传入,构造完成  
SpannableString word = new SpannableString("欢迎光临Harvic的博客");  
  
//使用SpannableStringBuilder,可以使用append()再添加  
SpannableStringBuilder multiWord = new SpannableStringBuilder();  
multiWord.append("欢迎光临");  
multiWord.append("Harvic的");  
multiWord.append("博客");  

20171105150981357471011.png
20171105150981357471011.png

因为Spannable等最终都实现了CharSequence接口,所以可以直接把SpannableString和SpannableStringBuilder通过TextView.setText()设置给TextView。

SetSpan()

void setSpan (Object what, int start, int end, int flags)

函数意义:给SpannableString或SpannableStringBuilder特定范围的字符串设定Span样式,可以设置多个(比如同时加上下划线和删除线等),Falg参数标识了当在所标记范围前和标记范围后紧贴着插入新字符时的动作,即是否对新插入的字符应用同样的样式。(这个后面会具体举例说明)

参数说明

  • object what :对应的各种Span,后面会提到;
  • int start:开始应用指定Span的位置,索引从0开始
  • int end:结束应用指定Span的位置,特效并不包括这个位置。比如如果这里数为3(即第4个字符),第4个字符不会有任何特效。从下面的例子也可以看出来。
  • int flags:取值有如下四个
  • Spannable.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括,即在指定范围的前面和后面插入新字符都不会应用新样式
  • Spannable.SPAN_EXCLUSIVE_INCLUSIVE :前面不包括,后面包括。即仅在范围字符的后面插入新字符时会应用新样式
  • Spannable.SPAN_INCLUSIVE_EXCLUSIVE :前面包括,后面不包括。
  • Spannable.SPAN_INCLUSIVE_INCLUSIVE :前后都包括。

举个例子来说明这个前后包括的问题:
由于Flag的作用是用来指定范围前后输入新的字符时,会不会应用效果的,所以我们利用EditText来显示SpannableString

(1)、布局XML中加入一个EditText控件:

<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="com.example.try_spannable_blog.MainActivity" >  
  
    <EditText  
        android:id="@+id/edit"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content" />  
  
</RelativeLayout>  

(2)、这里用一个改变字体颜色的Span来做下演示

public class MainActivity extends Activity {  
      
    private EditText editText;    
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
          
        editText = (EditText)findViewById(R.id.edit);  
          
        //改变字体颜色  
        //先构造SpannableString  
        SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
       //再构造一个改变字体颜色的Span  
        ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);    
        //将这个Span应用于指定范围的字体  
        spanString.setSpan(span, 1, 3, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);    
        //设置给EditText显示出来  
        editText.setText(spanString);  
    }  
}  

初始化效果是这样的:

2017110515098137022646.png
2017110515098137022646.png

分别在设置Span的前面和后面加入新文字,结果是这样的

20171105150981371612095.png
20171105150981371612095.png

在前面和后面都加入虾米两个字,可见,前面的虾米没有任何效果,后面的则不同,添加上相同的Span特效,这是由于我们设置了Spannable.SPAN_EXCLUSIVE_INCLUSIVE的原因,即(前面不应用特效,后面应用特效),其它几个Flags参数的含义想必大家也都清楚了。在此就不再赘述。

各种Span设置

在前面的一个小示例,大家应该也可以看出,要应用一个Span总共分三步:

  1. 构造String
  2. 构造Span
  3. 利用SetSpan()对指定范围的String应用这个Span

字体颜色设置(ForegroundColorSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
//再构造一个改变字体颜色的Span  
ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);    
//将这个Span应用于指定范围的字体  
spanString.setSpan(span, 1, 5, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);    
//设置给EditText显示出来  
editText.setText(spanString);  

效果:

20171105150981376259433.png
20171105150981376259433.png

字体背景颜色(BackgroundColorSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);    
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
editText.setText(spanString);    

20171105150981378799995.png
20171105150981378799995.png

字体大小(AbsoluteSizeSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
AbsoluteSizeSpan span = new AbsoluteSizeSpan(16);    
spanString.setSpan(span, 2, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);    
editText.setText(spanString); 

20171105150981381112225.png
20171105150981381112225.png

粗体、斜体(StyleSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
StyleSpan span = new StyleSpan(Typeface.BOLD_ITALIC);    
spanString.setSpan(span, 1, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
editText.setText(spanString);   

20171105150981385959014.png
20171105150981385959014.png

删除线(StrikethroughSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
StrikethroughSpan span = new StrikethroughSpan();    
spanString.setSpan(span, 2, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
editText.setText(spanString);

20171105150981388447056.png
20171105150981388447056.png

下划线(UnderlineSpan)

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
UnderlineSpan span = new UnderlineSpan();    
spanString.setSpan(span, 1, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
editText.setText(spanString); 

20171105150981391043913.png
20171105150981391043913.png

图片置换(ImageSpan)

ImagSpan有很多构造函数,一般是通过传入Drawableg来构造,详细的构造说明看这里:http://developer.android.com/reference/android/text/style/ImageSpan.html

SpannableString spanString = new SpannableString("欢迎光临Harvic的博客");    
Drawable d = getResources().getDrawable(R.drawable.ic_launcher);    
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());    
ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);    
spanString.setSpan(span, 2, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
editText.setText(spanString);  

20171105150981394222312.png
20171105150981394222312.png

这个函数的不同之处在于,前几都是在原来文字的基础上加上特效,而这里却是利用图片将文字替换。如果遇到不支持显示图片的函数,比如前一篇中的canvas绘图。就会退化成String,即以原来的String字符串来显示。

2017/10/10 posted in  Android

Android 基于开源项目搭建属于自己的技术堆栈

这篇博文主要就是针对平常使用到的框架做一个整理和分析其优劣。

为了从整体上进行把握,先来看看一个完整的APP整体架构

APP的整体架构

从较高的层次将,一个APP的整体架构可以分为两层,即应用层和基础框架层。

  • 应用层专注于行业领域的实现,例如金融、支付、地图导航、社交等,它直接面向用户,是用户对产品的第一层感知。
  • 基础框架层专注于技术领域的实现,提供APP公有的特性,避免重复制造轮子,它是用户对产品的第二层感知,例如性能、稳定性等。

一个理想的APP架构,应该拥有如下特点

  • 支持跨平台开发
  • 具有清晰的层次划分,同一层模块间充分解耦,模块内部符合面向对象设计六大原则
  • 在功能、性能、稳定性等方面达到综合最优

基于以上设计原则,我们可以看出APP架构图,最上层是应用层,应用层以下都属于基础框架层,基础框架层包括:组件层、基础层和跨平台层。

2017110415098088003059.png
2017110415098088003059.png

我们要讨论的重点是基础层,下面开始一步一步地阐述如何基于开源函数库搭建属于自己的一个基础技术堆栈。

技术选型的考量点

首先要明确的是,我们选择开源函数库或者第三方SDK、一般需要综合考虑一下几个方面

  • 特性:提供的特性是否满足项目的需求
  • 可用性,是否提供了简洁便利的API,方便开发者集成使用。
  • 性能:性能不能太差,否则项目后面性能优化会过不去,可能回出现需要替换函数库的情况。
  • 文档:文档应该比较齐全,且可读性高。
  • 技术支持:遇到问题或者发现BUG,是否能够及时得到官方的技术支持是很重要的
  • 大小:引入函数库会增加APK的大小,需要慎重抉择
  • 方法数:如果函数库方法数太多,积累起来会导致你的APP遇到64K问题,应该尽量避免

日志记录能力

日志记录无论在服务端开发还是移动端开发,都是一个基础且重要的能力,开发人员在代码调试以及错误定位过程中,大多说都要依赖日志信息,一个简洁灵活的日志记录模块是相当重要的。

Logger 是基于系统Log类基础上进行的封装,但新增了如下超赞的特性。

  • 在Logcat中完美的格式化输出,再也不用担心和手机其他APP或者系统的日志信息相混淆了
  • 包含线程、类、方法信息,可以清楚地看到日志记录的调用堆栈
  • 支持跳转到源码处
  • 支持格式化输出JSON、XML格式信息

Logcat截图

20171104150980887746260.png
20171104150980887746260.png

当然Logger也不是完备的,它虽然支持格式化输出JSON、XML,但并不支持诸如List、Set、Map和数组等常见Java集合类的格式化输出。如何解决呢?可以看下LogUtils 这个开源库,它实现了Logger缺失的上述特性。

再者,Logger只支持输出日志到Logcat,但项目开发中往往还存在将日志保存到磁盘上的需求,如何将两者结合起来呢?这是就遇到了timber 。

timber是JakeWharton开源的一个日志记录库,它的特点是可扩展的框架,开发者可以方便快捷的集成不同类型的日志记录方式,例如,打印日志到Logcat、打印日志到文件、打印日志到网络等,timber通过一行代码就可以同时调用多种方式。

timber的思想很简单,就是维护一个森林对象,它由不同类型的日志树组合而成,例如,Logcat记录树、文件记录树、网络记录树等,森林对象提供对外的接口进行日志打印。每种类型的树都可以通过种植操作把自己添加到森林对象中,或者通过移除操作从森林对象中删除,从而实现该类型日志记录的开启和关闭。

最终我们的日志记录模块将由timber+Logger+LogUtils组成,当然轮子找到了,轮子的兼容合并就得靠我们自己实现了,同时我们还得增加打印到文件的日志树和打印到网络的日志树实现。

JSON解析能力

移动互联网产品与服务器端通信的数据格式,如果没有特殊需求的话,一般都使用JSON格式。Android系统也原生的提供了JSON解析的API,但是它的速度非常慢,而且没有提供简洁方便的接口来提高开发者的效率和降低出错的可能。所以我们就开始找第三方开源库来实现JSON解析,比较优秀的包括如下几种。

gson

gosn是Google出品的JSON解析函数库,可以将JSON字符串反序列化对应的Java对象,或者反过来将Java对象序列化为对应的JSON字符串,免去了开发者手动通过JSONObject和JSONArray将JSON字段逐个进行解析的烦恼,也减少了出错的可能性,增强了代码的质量。使用gson解析时,对应的Java实体类无需使用注解进行标记,支持任意复杂Java对象包括没有源代码的对象。

jackson

jcakson是Java语言的一个流行的JSON函数库,在Android开发中使用时,主要包含三部分。

  • jackson-core:JSON流处理核心库
  • jackson-databind:数据绑定函数库,实现Java对象和JSON字符串流的相互转换。
  • jackson-annotations:databind使用的注解函数库

由于jackson是针对Java语言通用的JSON函数库,并没有为Android优化定制过,因此函数保重包含很多非必要的API,相比其他的JSON函数库,用于Android平台会更显著的增大最终生成的APK的体积。

Fastjson

Fastjson是阿里巴巴出品的一个Java语言编写的高性能且功能完善的JSON函数库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,号称是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。

由于是Java语言通用的,因此,以前在Android上使用时,Fastjson不可避免的引入了很多对于Android而言冗余的功能,从而增加了包大小,很多人使用的就是标准版的fastjson,但事实上,fastjson还存在一个专门为Android定制的版本---fastjson.android 。和标准版本相比,Android版本去掉了一些Android虚拟机dalvik不支持的功能,使得jar更小。

LoganSquare

LoganSquare是近两年崛起的快速解析和序列化JSON的Android函数库,其底层基于jackson的streaming API,使用APT(Android Annotation Tool)实现编译时注解,从而提高JSON解析和序列化的性能。官网上可以看到LoganSquare和gson、jackson databind的性能对比。

20171104150980902729820.jpg
20171104150980902729820.jpg

从性能方面看,LoganSquare是完胜gson和jackson的。如果和fastjson相比较,两者应该是不相上下的。

再来看下jar包的大小

  • gson:232KB
  • jackson:259+47+1229 = 1.5M
  • Fastjson:417KB
  • Fastjson.android:256KB
  • LoganSquare:48+259 = 307KB

从性能和包大小综合考虑,最终我们会选择Fastjson.android作为基础技术堆栈中的JSON解析和序列化库。

数据库操作能力

无论是iOS还是Android,底层数据库都是基于开源的SQLite实现,然后在系统层封装成用于应用层的API。虽然直接使用系统的数据库API性能很高,但是这些API接口并不是很方便开发者使用,一不小心就会引入Bug,而且代码的视觉效果也不佳。为了解决这个问题,对象关系映射(ORM)框架出现了,比较好的有ActiveAndroid,ormlite和greenDAO。

ActiveAndroid

ActiveAndroid是一种Active Record风格的ORM框架,Active Record(活动目录)是Yii,Rails等框架中对ORM实现的典型命名方式。它极大的简化数据库的使用,使用面向对象的方式管理数据库,告别手写SQL的历史。每一个数据库表都可以被映射为一个类,开发者只需使用类似save()或者delete()这样的函数即可。

不过ActiveAndroid已经基本上处于维护阶段了,最新的一个Release版本是在2012年发布的。

ormlite

ormlite是Java平台的一个ORM框架,支持JDBC连接、Spring和Android平台。在Android中使用时,它包含两部分。

  • ormlite-core:核心模块,无论在哪个平台使用,都必须基于这个核心库,是实现ORM映射的关键模块。
  • ormlite-android:基于ormlite-core封装的针对Android平台的适配器模块,Android开发中主要跟这个模块打交道。
    与ActiveAndroid类似,ormlite也已经不是一个活跃的开源库,最近一次Release版本是在2013年发布的。

greenDAO

greenDAO是一个轻量级且快速的ORM框架,专门为Android高度优化和定制,它能够支持每秒数千条记录的CRUD操作。官网上给出一张性能对比图

20171104150980910467767.png
20171104150980910467767.png

纵轴表示每秒执行的操作数。而且greenDAO处在高度活跃中,最新Release版本是在2017年3月份发布的

Realm

Realm是一个全新的移动数据库引擎,它既不是基于iOS平台的Core Data,也不是基于SQLite,它拥有自己的数据库存储引擎,并实现了高效快速的数据库构建操作,相比Core Data和SQLite,Realm操作要快很多,跟ORM框架相比就更不用说了。

Realm的好处如下:

  • 跨平台:Android和iOS已经是事实上的两大移动互联网操作系统,绝大多数应用都会支持这两个平台。使用Realm,Android和iOS开发者无需考虑内部数据的架构,调用Realm提供的API即可轻松完成数据的交换。
  • 用法简单:相比Core Data和SQLite所需的入门知识,Realm可以极大降低开发者的学习成本,快速实现数据库存储功能。
  • 可视化操作:Realm为开发者提供了一个轻量级的数据库可视化操作工具,开发者可以轻松查看数据库中的内容,并实现简单地插入和删除等操作。

我们看下上述四种数据库包大小。

  • activeandroid:40KB
  • greendao:100KB
  • ormlite-android:57KB
  • realm-android:4.2M

可以看出,前三个还是正常范围,但Realm的大小一般项目可能无法接受。这是因为不同CPU架构平台的 .so 文件增加了整个包的大小,由于arm平台的so在其他平台上面能够以兼容模式运行的,虽然会损失性能,但是可以极大地减少函数库占用的空间。因此,可以选择只保留armeabi-v7a和x86两个平台的 .so 文件,直接删除无用的 .so 文件,或者通过工程的build.gradle文件中增加 ndk abi 过滤,语句如下:

android {
    ...
    defaultConfig {
        ...
        ndk {
            abiFilters "armeabi-v7a", "x86"
        } 
    }
}

因此,综合性能考虑,包大小以及开源库的可持续发展等因素,我们最终选择greenDAO。

网络通信能力

现在的APP几乎都需要从服务器获取数据,不可避免的需要具备网络通信的能力,否则就是一个死界面。

android-async-http

Android最经典的网络异步通信函数库,它对Apache的HttpClient API的封装使得开发者可以简洁优雅地实现网络请求和响应,并且同时支持同步和异步请求。主要特性如下:

  • 支持异步HTTP请求,并在匿名回调函数中处理响应
  • 在子线程中发起HTTP请求
  • 内部采用线程池来处理并发请求
  • 通过RequestParams类实现GET/POST参数构造
  • 无需第三方库支持即可实现Multipart文件上传
  • 库的大小只有60KB
  • 支持多种移动网络环境下自动智能的请求重试机制
  • HTTP响应中实现自动的gzip解码,实现快速请求响应
  • 内置多种形式的响应解析,有原生的字节流、String、JSON对象,甚至可以将response写入到文件中。
  • 可选的永久cookie保存,内部实现使用的是Android的SharedPreferences。

但是在6.0之后,系统对开发者隐藏了HttpClient函数库,这显著增大了使用android-async-http的代价。 如果铁了心想继续使用HttpClient,官方推荐的做法是在编译期引入org.apache.http.legacy 这个库,库目录在Android SDK目录下的platforms\android-23\optional中找到,它的作用是确保在编译时不会出现找不到HttpClient相关API的错误,在应用运行时可以不依赖这个库,因为6.0以上的Android系统还没有真正移除HttpClient的代码,只不过API设置为对开发者不可见。我们查看android-async-http源码发现,需要使用下面这个函数库来替换之前的Apache的HttpClient。

dependencies {
    compile 'cz.msebera.android:httpclient:4.3.6'
}

这样显著的增加了APP的包的大小,如果想继续使用android-async-http,那么你的APP需要额外增加1.1MB左右的大小。

OkHttp

OkHttp是一个高效的HTTP客户端,具有如下特性。

  • 支持HTTP/2和SPDY,对同一台主机的所有请求共享同一个socket。
  • 当SPDY不可用时,使用连接池减少请求的延迟。
  • 透明的GZIP压缩减少下载数据大小
  • 缓存响应避免重复的网络请求

OkHttp在网络性能很差的情况下能够很好地工作,它能够避免常见的网络连接问题。如果你的HTTP服务有多个IP地址,OkHttp在第一次连接失败是,会尝试其他可选的地址。这对于IPv4+IPv6以及托管在冗余数据中心的服务来说是必要的。OkHttp使用现代的TLS特性(SNI,ALPN)初始化HTTP连接,当握手失败时,会降低使用TSL1.0初始化连接。

OkHttp依赖于okio,okio作为java.io和java.nio的补充,是square公司开发的一个函数库。okio使得开发者可以更好地访问、存储和处理数据。一开始是作为OkHttp的一个组件存在的,当然我们也可以单独使用它。

使用Okhttp需要引入Jar包,包的大小为:326+66 = 392KB

Volley

Volley是Google在2013年发布的用于Android平台的网络通信库,能使网络通信更快、更简单、更健壮。官网配出一张弓箭发射图来说明Volley特别使用于数据量小等通信频繁的场景。

具体的将,Volley是为了简化网络任务而设计的,用于帮助开发者处理请求、加载、缓存、多线程、同步等任务。Volley设计了一个灵活的网络栈适配器,在Android2.2及之前的版本中,Volley底层使用Apache HttpClient,在Android2.3及以上版本中,它使用HttpURLConnection来发起网络请求,而且开发者也很容易将网络栈切换成使用OkHttp。
Volley 官方源码托管在Google Source上面,使用时只能直接以Jar包形式引入,如果想在Gradle中使用compile在线引入,可以考虑使用mcxiaoke在Github上面的Volley Mirror,然后再build.gradle中使用如下语句即可。

compile 'com.mcxiaoke.volley:library:1.0.19'

Retrofit

确切的说,Retrofit并不是一个完整的网络请求函数库,而是将REST API转换成Java接口的一个开源函数库,它要求服务器API接口遵循REST规范。基于注解使得代码变得很简洁,Retrofit默认情况下使用GSON作为JSON解析器,使用OkHttp实现网络请求,三者通常配合使用,当然我们也可以将这两者换成其他的函数库。

通过以上分析,HttpURLConnection、Apache HttpClient 和OkHttp封装了底层的网络请求,而android-async-http,Volley和Retrofit是基于前面三者的基础上二次开发而成。

最后看下函数库的大小

  • android-async-http:106KB+1.1MB = 1.2MB
  • OkHttp:326KB+66KB = 392KB
  • Volley:94KB
  • Retrofit:122KB+211KB = 333KB

图片缓存和显示能力

图片缓存函数库有很多非常优秀的,开发人员可以根据需求进行选择。传统的图片缓存方案中设置有两级缓存,分别是内存缓存和磁盘缓存。在Facebook推出的Fresco中,它增加了一级缓存,也就是Native缓存,这极大地降低了使用Fresco的APP出现OOM的概率。

BitmapFun

BitmapFun函数库是Android官方教程中的一个图片加载和缓存实例,对于简单的图片加载需求来说,使用BitmapFun就够了,在早期用的多,现在渐渐退出了实际项目开发的舞台。

Picasso

Picasso是著名的square公司众多开源项目中的一个,它除了实现图片的下载和二级缓存功能,还解决了常见的一些问题。

  • 在adapter中正常的处理ImageView回收和下载的取消
  • 使用尽量小的内存实现复杂的图像变换

在Picasso中,我们使用一行代码即可实现图片下载并渲染到ImageView中。

Picasso.with(context).load(url).into(imageView);

Glide

Glide是Google推荐的用于Android平台上的图片加载和缓存函数库。这个库被广泛应用在Google的开源项目中,Glide和Picasso有90%的相似度,只是在细节上还是存在不少区别。Glide为包含图片的滚动列表做了尽可能流畅的优化。除了静态图片,Glide也支持GIF格式图片的显示。Glide提供了灵活的API可以让开发者方便地替换下载图片所用的网络函数库,默认情况下,它使用HttpUrlConnection作为网络请求模块,开发者也可以根据自己项目的实际需求灵活使用Google的Volley或者Square的OkHttp等函数库进行替换。

Glide的使用也可以使用一行代码来完成,语句如下

Glide.with(context).load(url).into(imageView);

Fresco

Fresco是Facebook开源的功能强大的图片加载和缓存函数库,相比其他图片缓存库,Fresco最显著的特点是具有三级缓存:两级内存缓存和一级磁盘缓存。主要特性如下:

  • 渐进式地加载JPEG图片
  • 显示GIF和WebP动画
  • 可扩展,可自定义图片加载和显示
  • 在Android 4.X和一下的系统上,将图片放在Android内存一个特殊的区域,从而使得应用运行更流畅,同时极大减低出现OutOfMemoryError的错误。

Android-Universal-Image-Loader

Android-Universal-Image-Loader简称UIL,是Android平台老牌的图片下载和缓存函数库,功能强大灵活且高度可自定义,它提供一系列配置选项,并能很好地控制图片加载和缓存的过程。使用者甚多,现在项目仍在使用。UIL也支持二级缓存,特性如下:

  • 同步或异步的多线程图片加载
  • 高度可自定义:线程池、下载器、解码器、内存和磁盘缓存、图片显示选项等。
  • 每张图片的显示支持多种自定义选项:默认存根图片、解码选项、Bitmap处理和显示等。
  • 图片可缓存在内存或者磁盘(设备的文件系统或者SD卡)上。
  • 可实时监听图片加载流程,包括下载进度。

最后看下几个库的包大小

  • BitmapFun:71KB
  • Picasso:120KB
  • Glide:475KB
  • Fresco:47KB+93KB+93KB+10KB+3MB+62KB+8KB+111KB = 3.4MB
  • Android-Universal-Image-Loader:162KB

图片函数库的选择需要根据APP的具体情况而定,对于严重依赖图片缓存的APP,例如壁纸类,图片社交类APP来说,可以选择最专业的Fresco。对于一般的APP,选择Fresco会显得比较重,毕竟Fresco 3.4MB的体量摆在这。

根据APP对图片显示和缓存的需求从低到高我们可以对以上函数库做一个排序

BitmapFun < Picasso < Android-Universal-Image-Loader < Glide < Fresco

值得一提的是,如果你的APP计划使用React Native进行部分模块功能的开发的话,那么在基础函数库选择方面需要考虑和React Native的依赖库的复用,这样可以减少引入React Native 所增加的APP的大小,可以复用的函数库有:OkHttp,Fresco,jackson-core.

2017/10/7 posted in  Android

Android 使用 Palette 让你的 UI 色彩与内容更贴合

前言

今天介绍一个 Android 下比较有意思的 Support v7 库,Palette,它翻译过来就是调色板。

Palette 可以从一张 Bitmap 中提取出它突出的颜色,这样我们就可以将提取出来的颜色设置在 App 的固定 UI 中(例如:ToolBar 的背景),使得 UI 页面的整体风格更加的美观和融洽。

比如,对于一些影视类的 App,视频详情页的主题都是视频的海报,那么对于页面背景,我们可以提取视频海报的颜色,设置在背景上,使得效果更佳柔和美观。

Palette 是一个 Support v7 的包,如果使用 Gradle 引入依赖,这里使用最新的 26.+。

compile "com.android.support:palette-v7:26.+"

Palette 的使用

Palette 使用起来非常的简单,既然目的是从一个图片中提取颜色,它的步骤就有:

  1. 传递一个 Bitmap,得到一个 Palette。
  2. 通过 Palette 提取需要的颜色。

就是这么简单,如同要将大象放冰箱,需要几步一样清晰。

那么接下来我们先来了解它使用的细节。

传递 Bitmap 得到一个 Palette

Palette 在旧版本上有一些 generate() 的方法,用于生成一个 Palette 对象,但是在新版本上已经被标记为 @Depercated 了,所以这里不推荐使用。

而在新版上,推荐使用 Palette.Builder 来创建我们的 Palette 对象,我们可以通过 from() 方法使用它。

20171104150979158539416.jpg
20171104150979158539416.jpg

一般我们使用第一个方法即可,直接传递进去一个 Bitmap 对象。得到 Builder 之后,我们还可以配置一些规则,但是一般我们不需要进行额外的(后面会讲到)。再通过 Builder.generate() 即可得到我们需要的 Palette 对象了。

通过 Palette 提取颜色

Palette 从图片中提取的颜色,有很多选择。这里又涉及到另外一个类,Swatch 。

Palette 可被提取的每个颜色,都被封装成一个 Swatch 对象,用来管理多种颜色。

这些 Swatch 有:

  • DominantSwatch
  • VibrantSwatch
  • DarkVibrantSwatch
  • LightVibrantSwatch
  • MutedSwatch
  • DarkMutedSwatch
  • LightMutedSwatch

其实这些 Swatch,真的不太好解释其意义,唯一特别一点的就是 DominantSwatch ,它是从图片中提取的最突出的颜色。

这些 Swatch 在 Palette 都提供了对应的 getXxx() 方法获得。不过需要注意的是,这些 getXxx() 方法可能会得到一个 null ,因为有些颜色是没有的。

如果只是需要得到一个颜色值,Palette 同时也提供了对应的 getXxxColor() 方法,方便我们使用。

得到 Swatch 对象之后,就可以通过对应的 Swatch 中对应的 Api 获取我们需要的颜色值。

  • getPopulation() :Swatch 中的像素个数。
  • getRgb():颜色的 RGB 值。
  • getHsl():颜色的 HSL 值。
  • getBodyTextColor():对应的文字颜色值。
  • getTitleTextColor():对应的标题文字颜色值。

通常来说,我们只需要通过 getRgb() 获取到对应的颜色设置在背景上,如果背景之上还有文字内容,可以通过 getBodyTextColor() 提取出与背景匹配的文字颜色值,这样可以显得更加的柔和,让文字看起来更清晰和舒服。比如,如果一个深色的背景,为它设置一个默认的深色文字,基本上就看不见了,因为对比对太弱。

举个例子

到这里,基本上 Palette 的基本 Api 就讲解清楚了,下面举个实际的例子来看看。

这里找了三张 Eason 的海报,用于做 Palette 的 Demo 资源,间隔去替换图片,然后分别提取出对应的颜色和字体颜色,设置在下面按钮的背景上,然后每 3s 切换一张图片。

因为有一些图片,获取的 Swatch 可能会返回 null ,所以这里用了一个比价扎眼的红色,作为错误色。

以下是获取 Swatch 的代码。

20171104150979172574268.jpg
20171104150979172574268.jpg

接下来通过 Swatch 提取我们需要的颜色。

20171104150979175346480.jpg
20171104150979175346480.jpg

这里分别获取了需要的颜色以及字体颜色,下面看看运行的效果:

20171104150979178525822.gif
20171104150979178525822.gif

可以看到,确实有一些颜色,被标记成了红色,说明当前图片有获取不到的对应颜色。

分析 Palette 的实现

Palette 的主线逻辑

继续深入看看 Palette 的实现原理,先从主线开始看。

Builder.generate() 开始。

20171104150979183575637.jpg
20171104150979183575637.jpg

从代码中可以看到,在 generate() 中,主线逻辑:

  1. 首先会通过 scaleBitmapDown() 方法,将图片压缩成一个小像素的,等于生成了一个新的 Bitmap 对象,这样有利于内存的管理,并且也减少了计算量。
  2. 然后再通过 mRegion 判断是否只是提取图片的某个区域,默认是完整的图片全部提取,当然也可以对 mRegion 进行配置。
    之后再构造一个 ColorCutQuantizer 对象,使用它的 getQuantizedColors() 方法得到 Swatch。
  3. 使用完前面压缩的 Bitmap 对象之后,再使用 recycle() 将其回收掉。
  4. 最后,通过 Palette 本身的构造函数,去生成一个 Palette 对象,返回出去。

接下来看看比较关键的 ColorCutQuantizer 中的实现逻辑。

20171104150979200867081.jpg
20171104150979200867081.jpg

从代码中可以看到,其中的逻辑还是很清晰的。

  1. 首先通过 quantizeFromRgb888() 方法,将每个像素的颜色进行量化,类似于将每个颜色取一个靠近的设置。举个不恰当的例子,将不同深度的红,都标记成红色。
  2. 再通过 shouldIgnoreColor() 过滤掉不需要的颜色。
  3. 最终获取到的颜色,如果小于等于我们设置的 maxColors,就可以通过 approximateToRgb888() 生成一批 Swatch。
  4. 如果大于 maxColors,就再通过 quantizePixels() 去掉一些杂色。
  5. 无论如何,最终操作的就是这里得到的 mQuantizedColors 对象。

Swatch 的 Target

所有需要的 Swatch ,都是被 Target 对象所标记。不同的 Swatch 都是通过 Target 中标记的常量值,进行运算,得到行的颜色。

2017110415097921282334.jpg
2017110415097921282334.jpg

过滤掉不需要的颜色

Palette 可以设置一些我们不需要的颜色,让它们不参与运算。这里的过滤条件,通过 Filter 来设定,并且 Palette 也提供给了一个 DEFAULT_FALTER 来标记默认的过滤颜色。

20171104150979216238491.jpg
20171104150979216238491.jpg

可以看到,默认的 Filter 会过滤掉一些靠近黑和白的颜色。

当然,我们也可以自己定义 Filter ,并通过 Palette 中的 addFilter()clearFilters() 来管理它。

2017110415097964327573.jpg
2017110415097964327573.jpg

这里存储 Filter 的是一个 ArrayList ,所以我们是可以定义很多个 Filter 加入进去的,它们都会生效。

设置 MaxColor

在 ColorCutQuantizer 中,被使用的 maxColor ,主要用于标记需要使用的颜色个数。它是可以通过 maximumColorCount() 方法,进行设置的,如果不对其进行设定,默认值为 16。

20171104150979646212624.jpg
20171104150979646212624.jpg

理论上来说,这里设置的maxColor 的值越大,运算花费的时间就越长。而越小,可以被选择的色值也就越少。

所以最佳的做法是根据当前 Bitmap 的用途来决定,色彩越丰富的图,设置的 maxColor 越大,即可。不过正常来说也不需要额外的设定,默认的配置就挺好用了。

小结

到这里就分析完 Palette 的所有相关的内容,不要仅仅满足使用。实际上看了 Palette 的源码,对色彩的运算,也有了更加深入的了解。

2017/10/6 posted in  Android