[Android] FragmentPagerAdapter 和 FragmentStatePageAdapter区别

一、官方解释

FragmentPagerAdapter 

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.

FragmentStatePageAdapter

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.

二、通俗解释

1. 适用对象:

FragmentPagerAdapter适用static and less count数目Fragment

FragmentStatePageAdapter适用dynamic and more count数目Fragment

2. Fragment生命周期

FragmentPagerAdapter中的Fragment是Detached,即,仅仅是对应的View被回收,整个Fragment实例仍然存在。

FragmentStatePageAdapter中的Fragment是Removed/Destroyed,整个Fragment实例都被回收干掉。

3. 内存占用

FragmentPagerAdapter占用内存大,因为Fragment实例仍然存在。

FragmentStatePageAdapter不占用内存,但它并不是永远消失了,从名字中的State可以看出,系统会把Fragment的state作为Bundle通过savedInstanceState而保存下来,当我们重新回到该Fragment后,可以从onViewStateRestored中恢复。

参考:http://stackoverflow.com/questions/30235335/difference-between-fragmentpageradapter-with-viewpager-with-offscreenlimit-set-t

[Android] TextView中setPadding()方法失效,不起作用

项目中使用了一个第三方控件,很多人都有用过,PagerSlidingTabStrip,发现在4.4(Kitkat)之前的版本,每个tab会挤在一起,分析源码后,发现每个tab在设置了setPadding后根本就无效,网上一查,还真有坑。

原因是在每个tab调用了setPadding后,又在其他地方调用了setBackgroundResource(),这在4.4之前是有问题的,4.4及以后的版本google已经fix了这个bug。

// does not work
tv.setPadding(20, 20, 20, 20);
tv.setBackgroundResource(R.drawable.border);

// works
tv.setBackgroundResource(R.drawable.border);
tv.setPadding(20, 20, 20, 20);

所以,解决办法是,必须在setBackgroundResource之后调用setPadding()方法。

[Android]RecyclerView显示不出来,没有调用onCreateViewHolder(), onBindViewHolder()等

RecyclerView作为Android比较新的控件,本意是要取代ListView,其中的最大特点是可扩展性高,比ListView灵活性好,在使用的过程中会遇到一些莫名奇妙的问题,特别是刚开始使用,会碰到一些“坑”,比如标题所述,使用RecyclerView,发现RecyclerView没有显示出来,调试发现没用调用onCreateViewHolder(), onBindViewHolder()函数,甚至对应的Adapter构造函数也没调到。

对于该问题,主要有以下几种原因:

1. Adapter的getItemCount() 函数返回0,这个是最“愚蠢”的原因

2. RecyclerView没有调用下列函数

setLayoutManager(new LinearLayoutManager(this))

3. RecyclerView放在ScrollView中,这个原因比较隐藏,对应的解决方案除了不要放在ScrollView之中外,还有就是更新Android Support Library 到23.2,即在build.gradle中:

dependencies {
    ...
    compile 'com.android.support:appcompat-v7:23.2.+'
    compile 'com.android.support:cardview-v7:23.2.+'
}

当然可能还有其他原因,总之还是小心为好。

[Android] 判断ListView是否滑到顶部

该场景很多,下面是终极方法,特别对于API小于14的设备,可能要考虑顶部有padding的情况:

private boolean listIsAtTop()   {   
    if(listView.getChildCount() == 0) return true;
    return listView.getChildAt(0).getTop() == 0;
}

该处有详细讨论:

http://stackoverflow.com/questions/7318373/how-to-find-out-if-listview-has-scrolled-to-top-most-position

 

[Android] ListView有Header时的position情况

经常需要在ListView前面加个Header View,这时获取ListView的position会有问题。

1.首先确保addHeaderView方法必须得在setAdapter之前被调用。

2.在OnItemClickListener的

public void onItemClick(AdapterView<?> parent, View view, int position,long id)

方法中,position是从header开始计算的,包括了header。那么要获得除去header后的正确位置应该怎么做呢?

方法1,position减去 listView.getHeaderViewsCount().

例如想得到ListView中可视的第一条item的在数据中的索引,就用getFirstVisiblePosition()减去getHeaderViewsCount();

方法2,在onItemClick不要直接使用我们声明的adapter,而是用ListView里的那个decorated adapter。

获取它的方法就是调用parent.getAdapter()

参见:http://blog.csdn.net/faithmy509/article/details/11521903

3. 如果 listview 调用了一次 addHeaderView,则

listView.getFirstVisiblePosition();

listView.getLastVisiblePosition();

listView.getChildAt(pos);

会以 headerView 为第0个view,item 的 pos会从1开始。

Understanding Android’s LayoutInflater.inflate()

英文链接:https://www.bignerdranch.com/blog/understanding-androids-layoutinflater-inflate/

It’s easy to get comfortable with boilerplate setup code, so much so that we gloss over the finer details. I’ve experienced this with LayoutInflater (which coverts an XML layout file into corresponding ViewGroups and Widgets) and the way it inflates Views inside Fragment’s onCreateView() method. Upon looking for clarification in Google documentation and discussion on the rest of the web, I noticed that many others were not only unsure of the specifics of LayoutInflater’s inflate() method, but were completely misusing it.

Much of the confusion may come from Google’s vague documentation in regards to attachToRoot, the optional third parameter of the inflate() method.

Whether the inflated hierarchy should be attached to the root parameter? If false, root is only used to create the correct subclass of LayoutParams for the root view of the XML.

Maybe the confusion comes from a statement that ends in a question mark?

Confusion

The general gist is this: If attachToRoot is set to true, then the layout file specified in the first parameter is inflated and attached to the ViewGroup specified in the second parameter.

Then the method returns this combined View, with the ViewGroup as the root. When attachToRoot is false, the layout file from the first parameter is inflated and returned as a View. The root of the returned View would simply be the root specified in the layout file. In either case, the ViewGroup’s LayoutParams are needed to correctly size and position the View created from the layout file.

Passing in true for attachToRoot results in a layout file’s inflated View being added to the ViewGroup right on the spot. Passing in false for attachToRoot means that the View created from the layout file will get added to the ViewGroup in some other way.

Let’s break down both scenarios with plenty of examples so we can better understand.

attachToRoot Set to True

Imagine we specified a button in an XML layout file with its layout width and layout height set to match_parent.

<Button xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:id="@+id/custom_button">
</Button>

We now want to programmatically add this Button to a LinearLayout inside of a Fragment or Activity. If our LinearLayout is already a member variable, mLinearLayout, we can simply add the button with the following:

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

We specified that we want to inflate the Button from its layout resource file; we then tell the LayoutInflater that we want to attach it to mLinearLayout. Our layout parameters are honored because we know the Button gets added to a LinearLayout. The Button’s layout params type should be LinearLayout.LayoutParams.

The following would also be equivalent. LayoutInflater’s two parameter inflate() method automatically sets attachToRoot to true for us.

inflater.inflate(R.layout.custom_button, mLinearLayout);

Another appropriate use of passing true for attachToRoot is a custom View. Let’s look at an example where a layout file uses a <merge> tag for its root. Using a <merge> tag signifies that the layout file allows for flexibility in terms of the type of root ViewGroup it may have.

public class MyCustomView extends LinearLayout {
    ...
    private void init() {
        LayoutInflater inflater = LayoutInflater.from(getContext());
        inflater.inflate(R.layout.view_with_merge_tag, this);
    }
}

This is a perfect use for a true attachToRoot parameter. The layout file does not have a root ViewGroup in this example, so we specify our custom LinearLayout to be its root. If our layout file had a FrameLayout as its root instead of <merge>, the FrameLayout and its children would inflate as normal. Then the FrameLayout and children would get added to the LinearLayout, leaving the LinearLayout as the root ViewGroup containing the FrameLayout and children.

attachToRoot Set to False

Let’s take a look at when you would want to set attachToRoot to false. In this scenario, the View specified in the first parameter of inflate() is not attached to the ViewGroup in the second parameter at this point in time.

Recall our Button example from earlier, where we want to attach a custom Button from a layout file to mLinearLayout. We can still attach our Button to mLinearLayout by passing in false for attachToRoot—we just manually add it ourselves afterward.

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);

These two lines of code are equivalent to what we wrote earlier in one line of code when we passed in true for attachToRoot. By passing in false, we say that we do not want to attach our View to the root ViewGroup just yet. We are saying that it will happen at some other point in time. In this example, the other point in time is simply the addView() method used immediately below inflation.

The false attachToRoot example requires a bit more work when we manually add the View to a ViewGroup. Adding our Button to our LinearLayout was more convenient with one line of code when attachToRoot was true. Let’s look at some scenarios that absolutely require attachToRoot to be false.

A RecyclerView’s children should be inflated with attachToRoot passed in as false. The child views are inflated in onCreateViewHolder().

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    LayoutInflater inflater = LayoutInflater.from(getActivity());
    View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
    return new ViewHolder(view);
}

RecyclerViews, not us, are responsible for determining when to inflate and present its child Views. The attachToRoot parameter should be false anytime we are not responsible for adding a View to a ViewGroup.

When inflating and returning a Fragment’s View in onCreateView(), be sure to pass in false for attachToRoot. If you pass in true, you will get an IllegalStateException because the specified child already has a parent. You should have specified where your Fragment’s view will be placed back in your Activity. It is the FragmentManager’s job to add, remove and replace Fragments.

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);

if (fragment == null) {
    fragment = new MainFragment();
    fragmentManager.beginTransaction()
        .add(R.id.root_viewGroup, fragment)
        .commit();
}

The root_viewGroup container that will hold your Fragment in your Activity is the ViewGroup parameter given to you in onCreateView() in your Fragment. It’s also the ViewGroup you pass into LayoutInflater.inflate(). The FragmentManager will handle attaching your Fragment’s View to this ViewGroup, however. You do not want to attach it twice. Set attachToRoot to false.

public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
    …
    return view;
}

Why are we given our Fragment’s parent ViewGroup in the first place if we don’t want to attach it in onCreateView()? Why does the inflate() method request a root ViewGroup?

It turns out that even when we are not immediately adding our newly inflated View to its parent ViewGroup, we should still use the parent’s LayoutParams in order for the new View to determine its size and position whenever it is eventually attached.

You are bound to run into some poor advice about LayoutInflater on the web. Some people will advise you to pass in null for the root ViewGroup if you are going to pass in false for attachToRoot. However, if the parent is available, you should pass it in.

FrameLayout Root

Lint will now warn you not to pass in null for root. Your app won’t crash in this scenario, but it can misbehave. When your child View doesn’t know the correct LayoutParams for its root ViewGroup, it will try to determine them on its own using generateDefaultLayoutParams.

These default LayoutParams might not be what you desired. The LayoutParams that you specified in XML will get ignored. We might have specified that our child View should match the width of our parent View, but ended up with our parent View wrapping its own content and ending up much smaller than we expected.

There are a few scenarios in which you will not have a root ViewGroup to pass into inflate(). When creating a custom View for an AlertDialog, you do not yet have access to its parent.

AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
View customView = inflater.inflate(R.layout.custom_alert_dialog, null);
...
dialogBuilder.setView(customView);
dialogBuilder.show();

In this case, it is okay to pass in null for the root ViewGroup. It turns out that the AlertDialog would override any LayoutParams to match_parent anyway. However, the general rule of thumb is to pass in the parent if you’re able to do so.

Avoiding Crashes, Misbehaviors and Misunderstandings

Hopefully this post helps you avoid crashes, misbehaviors and misunderstandings when using LayoutInflater. Here are some key takeaways for different uses in certain circumstances:

  • If you have a parent to pass into the root ViewGroup parameter, do so.
  • Try to avoid passing in null for the root ViewGroup.
  • Pass in false for the attachToRoot parameter when we are not the ones responsible for attaching our layout file’s View to its root ViewGroup.
  • Do not pass in true for a View that has already been attached to a ViewGroup.
  • Custom Views are a good use case to pass in true for attachToRoot.

Android Grid Layout

英文地址:https://medium.com/google-developer-experts/android-grid-layout-1faf0df8d6f2#.e8gk4csdt

Question which android developers ask them-self every day — which layout to use?

It has been a while since GridLayout released — New Layout Widgets: Space and GridLayout.

Current situation in android development world regarding GridLayout is following:

  • Most of android developers don’t even know such layout exist.
  • Some android developers know about GridLayout but for some reason do not use this layout.
  • Only few android developers spent time to play with GridLayout and are actively using it.

The reason I wrote this article, is because I think this layout has been unfairly forgotten.

Why do we need Grid Layout?

GridLayout gives you possibility to create grid based layouts with a single root view.

I can use nested LinearLayout to create grid!

Yes, but you can have performance problems in hierarchies that are too deep.

I can use RelativeLayout to create grid!

Yes, but RelativeLayout has some limitations, like:

  • inability to control alignment along both axes simultaneously.
  • widgets can get off the screen / overlap when they take more space than you expected because you can’t use weigh, etc.

In other words RelativeLayout — is not flexible and responsive enough.

Sample

Let’s implement a simple layout which consist of one big image, two small icons and text next to those icons.

Preview

RelativeLayout

It’s pretty easy to implement this layout via RelativeLayout. Key attributes here are layout_below, layout_toRightOf and layout_alignTop.

Code

At first glance everything seems to be perfect, until you start testing your layout with different text sizes.

Issue 1 inability to control alignment along both axes simultaneously.

Single line text should be vertically centered with icons, unfortunatelyRelativeLayout doesn’t offer such possibility.

Preview

Issue 2 widgets overlapping

Multi-line text cause overlapping, since text is aligned to icon vialayout_alignTop attribute.

Preview

GridLayout

As you can see on image below GridLayout produces much better results:

  • text is vertically aligned to icon.
  • multi-line text push widgets down.

Preview

So how to achieve such results? First define GridLayout as a root layout. Next count how many columns do you have and defineandroid:columnCount attribute, in this example we have 2 columns.

When you define views inside GridLayout they are placed one after another, so it’s not necessary to explicitly define view row and column.

If you want to stretch view for 2 or more rows / columns you can uselayout_columnSpan / layout_rowSpan attribute.

And the most important thing to remember — if you want your view to use all available space, don’t set width of your view to match_parent, set it to0dp along with layout_gravity=”fill” attribute.

Code

Conclusions

GridLayout can be a powerful tool in the right hands. It provides great flexibility and performance. On the other hand it requires some time learn how it works, you usually need more time to develop and maintain such layout.

Pros:

  • flexibility
  • single root layout

Cons:

  • learning curve
  • maintenance

Yelp app是如何使用Glide优化图片加载的

动态加载图片是很多安卓应用的基础。在Yelp(美国最大点评网站)中,图片在把消费者与商家联系起来的过程中至关重要。随着网络通信和硬件水平的越发强大,消费者对于图片数量和图片质量的期望日益增长。图片可以轻易的成为内存和网络流量的消耗大户,处理图片数据的下载和管理成为了一个让人望而却步的任务。我们探索了几种处理这个问题的解决办法,最终认为Glide在性能,使用方便性,稳定性上达到了相当好的平衡。

Glide最简单的使用案例就是从远程服务器或者本地文件系统加载图片,把它们放在磁盘与内存缓存中,然后加载到view上。它可以用在全市图片的app中,Glide为包含图片的滚动列表做了尽可能流畅的优化。

对象池

Glide原理的核心是为bitmap维护一个对象池。对象池的主要目的是通过减少大对象的分配以重用来提高性能(至于对象池的概览,可以查看 这个Android performance pattern 视频)。

Dalvik和ART虚拟机都没有使用compacting garbage collector,compacting garbage collector是一种模式,这种模式中GC会遍历堆,同时把活跃对象移到相邻内存区域,让更大的内存块可以用在后续的分配中。因为安卓没有这种模式,就可能会出现被分配的对象分散在各处,对象之间只有很小的内存可用。如果应用试图分配一个大于邻近的闲置内存块空间的对象,就会导致OutOfMemoryError,然后崩溃,即使总的空余内存空间大于对象的大小。

使用对象池还可以帮助提高滚动的性能,因为重用bitmap意味着更少的对象被创建与回收。垃圾回收会导致“停止一切(Stop The World)”事件,这个事件指的是回收器执行期间,所有线程(包括UI线程)都会暂停。这个时候,图像帧无法被渲染同时UI可能会停滞,这在滚动期间尤其明显。

Yelp app是如何使用Glide优化图片加载的

Glide的使用

Glide使用起来很简单,而且不需要任何特别的配置就自动包含了bitmap pooling 。

DrawableRequestBuilder requestBuilder = Glide.with(context).load(imageUrl);
requestBuilder.into(imageView);

这就是加载一张图片的全部要求。就像安卓中的很多地方一样,with() 方法中的context到底是哪种类型是不清楚的。有一点很重要需要记住,就是传入的context类型影响到Glide加载图片的优化程度,Glide可以监视activity的生命周期,在activity销毁的时候自动取消等待中的请求。但是如果你使用Application context,你就失去了这种优化效果。

译者注:其实以上的代码是一种比较规范的写法,我们更熟悉的写法是:

Glide.with(context)
    .load("http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg")
    .into(ivImg);

优化特性

类似的是,如果相关的item已经滚出了屏幕的范围,Glide会自动取消列表中的悬着的图片请求 。因为绝大多数开发者都会在adapter中利用view的回收,Glide做到这点是通过在ImageView上设置一个tag,在加载另外一张图片之前检查这个tag,如果存在就取消第一次请求。

Glide提供了几个让你感觉图片加载速度变快的特性。第一个就是在图片显示在屏幕上之前就预先取出图片。它提供了一个ListPreloader类, 它被应该事先取出的item数目实例化。然后通过setOnScrollListener(OnScrollListener).被传递给ListView。你想在ListView之外也能预先取出图片吗?没问题,使用前面的builder对象就可以了,只需调用builder.downloadOnly()。

downloadOnly见:https://github.com/bumptech/glide/wiki/Loading-and-Caching-on-Background-Threads 。

我们发现了Glide提供的可以大大提高性能,稳定性的功能,以及安卓图片加载领域的一些设计哲学。这些特性和优化确实可以很好的将图片加载的体验变成一种享受。

Android中深入理解 LayoutInflater.inflate()

由于我们很容易习惯公式化的预置代码,有时我们会忽略很优雅的细节。LayoutInflater以及它在Fragment的onCreateView()中填充View的方式带给我的就是这样的感受。这个类用于将XML文件转换成相对应的ViewGroup和控件Widget。我尝试在Google官方文档与网络上其他讨论中寻找有关的说明,而后发现许多人不但不清楚LayoutInflater的inflate()方法的细节,而且甚至在误用它。

这里的困惑很大程度上是因为Google上有关attachToRoot(也就是inflate()方法第三个参数)的文档太模糊:

被填充的层是否应该附在root参数内部?如果是false,root参数只是用于为XML根元素View创建正确的LayoutParams的子类。

其实意思就是:如果attachToRoot是true的话,那第一个参数的layout文件就会被填充并附加在第二个参数所指定的ViewGroup内。方法返回结合后的View,根元素是第二个参数ViewGroup。如果是false的话,第一个参数所指定的layout文件会被填充并作为View返回。这个View的根元素就是layout文件的根元素。不管是true还是false,都需要ViewGroup的LayoutParams来正确的测量与放置layout文件所产生的View对象。

attachToRoot传入true代表layout文件填充的View会被直接添加进ViewGroup,而传入false则代表创建的View会以其他方式被添加进ViewGroup。

让我们就两种情况多举一些例子来更深入的理解。

attachToRoot是True

假设我们在XML layout文件中写了一个Button并指定了宽高为match_parent。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/custom_button">
</Button>

现在我们想动态地把这个按钮添加进Fragment或Activity的LinearLayout中。如果这里LinearLayout已经是一个成员变量mLinearLayout了,我们只需要通过如下代码达成目标:

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

我们指定了用于填充button的layout资源文件,然后我们告诉LayoutInflater我们想把button添加到mLinearLayout中。这里Button的LayoutParams种类为LinearLayout.LayoutParams。

下面的代码也有同样的效果。LayoutInflater的两个参数的inflate()方法自动将attachToRoot设置为true。

inflater.inflate(R.layout.custom_button, mLinearLayout);

另一种在attachToRoot中传递true的情况是使用自定义View。我们看一个layout文件中根元素有标签的例子。标签标识着这个layout文件的根ViewGroup可以有多种类型。

public class MyCustomView extends LinearLayout {
    ...
    private void init() {
    LayoutInflater inflater = LayoutInflater.from(getContext());
    inflater.inflate(R.layout.view_with_merge_tag, this);
    }
}

这就是一个很好的使用attachToRoot的例子。这个例子中layout文件没有ViewGroup作为根元素,所以我们指定我们自定义的LinearLayout作为根元素。如果layout文件有一个FrameLayout作为根元素而不是,那么FrameLayout和它的子元素都可以正常填充,而后都会被添加到LinearLayout中,LinearLayout是根ViewGroup,包含着FrameLayout和其子元素。

attachToRoot是False

我们看一下什么时候attachToRoot应该是false。在这种情况下,inflate()方法中的第一个参数所指定的View不会被添加到第二个参数所指定的ViewGroup中。

回忆一下刚才的例子中的Button,我们想通过layout文件添加自定义的Button至mLinearLayout中。当attachToRoot为false时,我们仍可以将Button添加到mLinearLayout中,但是这需要我们自己动手。

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);

这两行代码与刚才attachToRoot为true时的一行代码等效。通过传入false,我们告诉LayoutInflater我们不暂时还想将View添加到根元素ViewGroup中,意思是我们一会儿再添加。在这个例子中,一会儿再添加就是在inflate()后调用addView()方法。

在将attachToRoot设置为false的例子中,由于要手动添加View进ViewGroup导致代码变多了。将Button添加到LinearLayout中还是用一行代码直接将attachToRoot设置为true简便一些。下面我们看一下什么情况下attachToRoot必须传入false。

每一个RecyclerView的子元素都要在attachToRoot设置为false的情况下填充。这里子View在onCreateViewHolder()中填充。

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    LayoutInflater inflater = LayoutInflater.from(getActivity());
    View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
    return new ViewHolder(view);
}

RecyclerView负责决定什么时候展示它的子View,这个不由我们决定。在任何我们不负责将View添加进ViewGroup的情况下都应该将attachToRoot设置为false。

当在Fragment的onCreateView()方法中填充并返回View时,要将attachToRoot设为false。如果传入true,会抛出IllegalStateException,因为指定的子View已经有父View了。你需要指定在哪里将Fragment的View放进Activity里,而添加、移除或替换Fragment则是FragmentManager的事情。

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);

if (fragment == null) {
    fragment = new MainFragment();
    fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}

上面代码中root_viewGroup就是Activity中用于放置Fragment的容器,它会作为inflate()方法中的第二个参数被传入onCreateView()中。它也是你在inflate()方法中传入的ViewGroup。FragmentManager会将Fragment的View添加到ViewGroup中,你可不想添加两次。

public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
    …
    return view;
}

问题是:如果我们不需在onCreateView()中将View添加进ViewGroup,为什么还要传入ViewGroup呢?为什么inflate()方法必须要传入根ViewGroup?

原因是及时不需要马上将新填充的View添加进ViewGroup,我们还是需要这个父元素的LayoutParams来在将来添加时决定View的size和position。

你在网上一定会遇到一些不正确的建议。有些人会建议你如果将attachToRoot设置为false的话直接将根ViewGroup传入null。但是,如果有父元素的话,还是应该传入的。

深入理解LayoutInflater.inflate()

Lint会警告你不要讲null作为root传入。你的App不会挂掉,但是可能会表现异常。当你的子View没有正确的LayoutParams时,它会自己通过generateDefaultLayoutParams)计算。

你可能并不想要这些默认的LayoutParams。你在XML指定的LayoutParams会被忽略。我们可能已经指定了子View要填充父元素的宽度,但父View又wrap_content导致最终的View小很多。

下面是一种没有ViewGroup作为root传入inflate()方法的情况。当为AlertDialog创建自定义View时,还无法访问父元素。

AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
View customView = inflater.inflate(R.layout.custom_alert_dialog, null);
...
dialogBuilder.setView(customView);
dialogBuilder.show();

在这种情况下,可以将null作为root ViewGroup传入。后来我发现AlertDialog还是会重写LayoutParams并设置各项参数为match_parent。但是,规则还是在有ViewGroup可以传入时传入它。

避开崩溃、异常表现与误解

希望这篇文章可以帮助你在使用LayoutInflater时避开崩溃、异常表现与误解。下面整理了文章的要点:

  • 如果可以传入ViewGroup作为根元素,那就传入它。
  • 避免将null作为根ViewGroup传入。
  • 当我们不负责将layout文件的View添加进ViewGroup时设置attachToRoot参数为false。
  • 不要在View已经被添加进ViewGroup时传入true。
  • 自定义View时很适合将attachToRoot设置为true。

谈谈Android中的Divider

在Android应用开发中会经常碰到一个叫divider的东西,就是两个View之间的分割线。最近工作中注意到这个divider并分析了一下,竟然发现内有乾坤,惊为天人…

ListView的divider

1. 定制divider的边距

ListView的divider默认是左右两头到底的,如何简单的设置一个边距呢?

利用inset或者layer-list都可以简单的实现,代码如下:

<!-- 方法一 -->
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
 android:insetLeft="16dp" >
    <shape android:shape="rectangle" >
        <solid android:color="#f00" />
    </shape>
</inset>
<!-- 方法二 -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:left="16dp">
        <shape android:shape="rectangle">
            <solid android:color="#f00" />
        </shape>
    </item>
</layer-list>

其中inset除了左边距insetLeft, 还有insetTop、insetRight、insetBottom, 效果图:

谈谈Android中的Divider

2. 最后一项的divider

很多同学可能发现了,ListView最后一项的divider有时候有,有时候又没有。

我画个图大家就都能理解了:

谈谈Android中的Divider

上面是数据不足的显示效果,如果数据满屏的话,都是看不多最后的divider的。

真相是,当ListView高度是不算最后一项divider的,所以只有在match_parent的情况下,ListView的高度是有余的,才能画出最后的那个divider。

ps:网上很多资料,把最后一项的divider和footerDividersEnabled混在一起了,这个是不对的,两个从逻辑上是独立的,类似的还有一个headerDividersEnabled,headerDividersEnabled和footerDividersEnabled不会影响到默认情况下最后的divider的绘制,他们是给header和footer专用的,特此说明。

RecyclerView的Divider

RecyclerView的Divider叫做ItemDecoration,RecyclerView.ItemDecoration本身是一个抽象类,官方没有提供默认实现。

官方的Support7Demos例子中有个DividerItemDecoration, 我们可以直接参考一下,位置在sdk的这里:

extras/android/support/samples/Support7Demos/src/…/…/decorator/DividerItemDecoration.java

但是这个DividerItemDecoration有三个问题:

  1. 只支持系统默认样式,不支持自定义Drawable类型的divider
  2. 里面的算法对于无高宽的Drawable(比如上面用到的InsetDrawable)是画不出东西的
  3. 水平列表的Divider绘制方法drawHorizontal()的right计算有误,导致垂直Divider会绘制不出来,应该改为:final int right = left + mDivider.getIntrinsicWidth();;

针对这几个问题,我修复并增强了一下:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 * RecyclerView的ItemDecoration的默认实现
 * 1. 默认使用系统的分割线
 * 2. 支持自定义Drawable类型
 * 3. 支持水平和垂直方向
 * 4. 修复了官方垂直Divider显示的bug
 * 扩展自官方android sdk下的Support7Demos下的DividerItemDecoration
 */
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
        android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;
    private int mWidth;
    private int mHeight;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    /**
 * 新增:支持自定义dividerDrawable
 *
 * @param context
 * @param orientation
 * @param dividerDrawable
 */
    public DividerItemDecoration(Context context, int orientation, Drawable dividerDrawable) {
        mDivider = dividerDrawable;
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    /**
 * 新增:支持手动为无高宽的drawable制定宽度
 * @param width
 */
    public void setWidth(int width) {
        this.mWidth = width;
    }

    /**
 * 新增:支持手动为无高宽的drawable制定高度
 * @param height
 */
    public void setHeight(int height) {
        this.mHeight = height;
    }

    @Override
        public void onDraw(Canvas c, RecyclerView parent) {
            if (mOrientation == VERTICAL_LIST) {
                drawVertical(c, parent);
            } else {
                drawHorizontal(c, parent);
            }
        }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + getDividerHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
            final int left = child.getRight() + params.rightMargin +
                Math.round(ViewCompat.getTranslationX(child));
            final int right = left + getDividerWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            if (mOrientation == VERTICAL_LIST) {
                outRect.set(0, 0, 0, getDividerHeight());
            } else {
                outRect.set(0, 0, getDividerWidth(), 0);
            }
        }

    private int getDividerWidth() {
        return mWidth > 0 ? mWidth : mDivider.getIntrinsicWidth();
    }

    private int getDividerHeight() {
        return mHeight > 0 ? mHeight : mDivider.getIntrinsicHeight();
    }

}

使用如下:

// 默认系统的divider
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
// 自定义图片drawable分的divider
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST, getResources().getDrawable(R.drawable.ic_launcher));
// 自定义无高宽的drawable的divider - 垂直列表
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST, new ColorDrawable(Color.parseColor("#ff00ff")));
dividerItemDecoration.setHeight(1);
// 自定义无高宽的drawable的divider - 水平列表
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL_LIST, new ColorDrawable(Color.parseColor("#ff00ff")));
dividerItemDecoration.setWidth(1);
// 自定义带边距且无高宽的drawable的divider(以上面InsetDrawable为例子)
// 这个地方也可以在drawable的xml文件设置size指定宽高,效果一样
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL_LIST, getResources().getDrawable(R.drawable.list_divider));
dividerItemDecoration.setWidth(DisplayLess.$dp2px(16) + 1);

手动的Divider

有的时候没有系统控件的原生支持,只能手动在两个view加一个divider,比如,设置界面每项之间的divider,水平平均分隔的几个view之间加一个竖的divider等等。

无论横的竖的,都非常简单,定一个View,设置一个background就可以了,正常情况下没什么好说的。

下面我们来考虑一种常见设置界面,这种设置界面的分割线是有左边距的,比如微信的设置界面,我相信绝大部分人的布局代码都是这样实现的:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

    <!--这个group_container的background一定要设置,
 而且要和list_item_bg的list_item_normal一致,
 否则效果会不正确。 -->
    <LinearLayout
 android:id="@+id/group_container"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentTop="true"
 android:layout_marginTop="48dp"
 android:background="#fff"
 android:orientation="vertical">

        <RelativeLayout
 android:id="@+id/account_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/account_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="First Item"
 android:textColor="#f00"
 android:textSize="16sp" />
        </RelativeLayout>

        <View
 android:layout_width="match_parent"
 android:layout_height="1px"
 android:layout_marginLeft="16dp"
 android:background="#f00" />

        <RelativeLayout
 android:id="@+id/phone_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/phone_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="Second Item"
 android:textColor="#f00"
 android:textSize="16sp" />

        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

效果图如下,顺便我们也看看它的Overdraw状态:

谈谈Android中的Divider

通过分析Overdraw的层次,我们发现为了一个小小的边距,设置了整个groud_container的背景,从而导致了一次Overdraw。

能不能优化掉这个Overdraw?答案是肯定的。

背景肯定要去掉,但是这个左边距的View就不能这么简单的写了,需要自定义一个View,它要支持能把左边距的空出的16dp的线用list_item_normal的颜色值绘制一遍,这样才能看的出左边距。

这个View具体代码如下:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.jayfeng.lesscode.core.R;

public class SpaceDividerView extends View {

    private int mSpaceLeft = 0;
    private int mSpaceTop = 0;
    private int mSpaceRight = 0;
    private int mSpaceBottom = 0;
    private int mSpaceColor = Color.TRANSPARENT;

    private Paint mPaint = new Paint();

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

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

    public SpaceDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SpaceDividerView, defStyleAttr, 0);
        mSpaceLeft = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceLeft,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceTop = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceTop,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceRight = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceRight,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceBottom = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceBottom,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceColor = a.getColor(R.styleable.SpaceDividerView_spaceColor, Color.TRANSPARENT);
        a.recycle();

        mPaint.setColor(mSpaceColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mSpaceLeft > 0) {
            canvas.drawRect(0, 0, mSpaceLeft, getMeasuredHeight(), mPaint);
        }
        if (mSpaceTop > 0) {
            canvas.drawRect(0, 0, getMeasuredWidth(), mSpaceTop, mPaint);
        }
        if (mSpaceRight > 0) {
            canvas.drawRect(getMeasuredWidth() - mSpaceRight, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        }
        if (mSpaceBottom > 0) {
            canvas.drawRect(0, getMeasuredHeight() - mSpaceBottom, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        }
    }
}

用这个SpaceDividerView我们重写一下上面的布局代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
 android:id="@+id/group_container"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentTop="true"
 android:layout_marginTop="48dp"
 android:orientation="vertical">

        <RelativeLayout
 android:id="@+id/account_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/account_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="First Item"
 android:textColor="#f00"
 android:textSize="16sp" />
        </RelativeLayout>

        <com.jayfeng.lesscode.core.other.SpaceDividerView
 android:layout_width="match_parent"
 android:layout_height="1px"
 android:background="#f00"
 app:spaceLeft="16dp"
 app:spaceColor="@color/list_item_normal"/>

        <RelativeLayout
 android:id="@+id/phone_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/phone_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="Second Item"
 android:textColor="#f00"
 android:textSize="16sp" />

        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

效果图和Overdraw状态如下:

谈谈Android中的Divider

界面中group_container那块由之前的绿色变成了蓝色,说明减少了一次Overdraw。

上述情况下,SpaceDividerView解耦了背景色,优化了Overdraw,而且这个SpaceDividerView也是支持4个方向的,使用起来特别方便。

阴影divider

阴影分割线的特点是重叠在下面的view之上的,它的目的是一种分割线的立体效果。

谈谈Android中的Divider

使用RelativeLayout并控制上边距离可以实现:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

    <!-- layout_marginTop的值应该就是不包括阴影高度的header高度-->
    <LinearLayout
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:layout_alignParentTop="true"
 android:layout_marginTop="@dimen/header_height"
 android:orientation="vertical">
    </LinearLayout>

    <!-- 这个要放在最后,才能显示在最上层,这个header里面包括一个阴影View-->
    <include
 android:id="@+id/header"
 layout="@layout/include_header" />
</RelativeLayout>

虽然再简单不过了,还是稍微分析一下,header包括内容48dp和阴影8dp,那么marginTop就是48dp了。