Auto Layout on Android, A Pure Java Implementation

If you are familiar with IOS development, Auto Layout probably is not something new to you. But for those pure android geeks and others who are interested: “Auto Layout is a system that lets you lay out your app’s user interface by creating a mathematical description of the relationships between the elements. You define these relationships in terms of constraints either on individual elements, or between sets of elements. Using Auto Layout, you can create a dynamic and versatile interface that responds appropriately to changes in screen size, device orientation, and localization.” Quoted from Apple.

By reading a verbose definition won’t help much, an example may enlighten the concept better:

autolayout

In the above screenshot, we have a layout container (which has the screen size), and 3 different objects (views). To arrange such a layout via Auto Layout on IOS, we may simply define a few constraints:

(assume <container.padding> is a constant that represents the spaces between objects)

“green.top == container.top + <container.padding>”
“green.left == container.left + <container.padding>”
“green.width == (container.width – 3 * <container.padding>)  /  2”
“green.height == (container.height – 3 * <container.padding>) / 2”
“yellow.top == green.top”
“yellow.left == green.right + <container.padding>”
“yellow.width == green.width”
“yellow.height == green.height”
“blue.top == green.bottom + <container.padding>”
“blue.left == green.left”
“blue.right == container.right – <container.padding>”
“blue.height == green.height”

After we pass this set of constraints to the layout manager, the layout engine will do the math and solve out a best result that can satisfy each of these constraints. Thus a beautiful layout is rendered based on the calculated positions of each object.

Now it’s the point of this article: this awesome layout engine is now available on Android, and you can find the source code at Github: cassowary layout. It named cassowary because that is the original name of the algorithm, which also runs behind the Auto Layout of IOS. Yes, this android version library and the Auto Layout are sharing the same algorithm, which was developed by a few computer scientists back in 1990’s. Actually, there is a whole community behind this cassowary algorithm today: overconstrained. In there developers from all over the world have contributed to different ports of the same algorithm, and some of those implementations are already in use of many applications we are using daily today.

It is worth to mention that Cassowary layout listed above includes some android elements to handle the transition from mathematical calculated results to the actual sizes and positions of different views. If you simply just want to use the pure java implementation, I would suggest you to take a look at the math engine used by this cassowary layout library: cassowary-java. Alternatively, I personally prefer this re-designed implementation of the same algorithm: kiwi-java, which is declared as faster and smaller.

All these great libraries can be found at the cassowary community’s website: overconstrained

 

Advertisements

A powerful parser for Java and Android: Jparsec

Today I want to talk about a powerful Java parser: Jparsec (http://jparsec.codehaus.org/). I recently used it for one of my projects and found it really helpful. This article is mainly for students, who are still at school and have no industry experience. If you are already in Java development career for years, then you might already a user of this tool or at least met it before.

Back in school, whenever I needed to parse strings to capture inputs for my projects, I usually took the shortcut to make the development faster, by using the easiest way: str.split(). Normally a str.contains() command would be enough to help me judge whether the input string is the format my code required, then splitting the string to an array of words by defining delimiters finished the job.

This is an efficient approach when the project is small enough and while the input string format is rigidly controlled. If I ever encountered a more complex case, constructing a regex would be my final weapon to launch. I had never been to a circumstance that these methods failed on me, since all the projects was not at the engineering level yet.

However, these approaches became unacceptable for my first job. str.split() can only handle cases when delimiters are not complex, and if you can’t control the input format, things get worse because you never know what the string from user input looks like. Regex is hard to write at the first place and it’s even harder to maintain because it’s not human readable when it gets long and complicated. A experienced team member recommended Jparsec to me and it becomes my daily solution for parsing at these days.

Accurately speaking, Jparsec is a parser building tool. It helps you build mini java parsers quickly. The standout of Jparsec is its combinator nature. You can start building simple parsers such as whitespace parser, and then combine these simple parsers to a more complex parser. The way it works is just like the evolution of functional programming: Taking advantage of simple functions to generate the most powerful function!

Let’s see some codes:

/*
    ** fundamental parsers
     */


    // zero or more whitespace
    private static final Parser<String> whiteSpace() {
        return Scanners.string(" ").many().source();
    }

    //left parentheses
    private static final Parser<String> leftParen() {
        return whiteSpace().followedBy(Scanners.string("(")).followedBy(whiteSpace());
    }

    //right parentheses
    private static final Parser<String> rightParen() {
        return whiteSpace().followedBy(Scanners.string(")")).followedBy(whiteSpace());
    }

    //comma
    private static final Parser<String> comma() {
        return whiteSpace().followedBy(Scanners.string(",")).followedBy(whiteSpace());
    }

    //dot
    private static final Parser<String> dot() {
        return whiteSpace().followedBy(Scanners.string(".")).followedBy(whiteSpace());
    }

    //negative sign
    private static final Parser<String> negativeSign() {
        return whiteSpace().next(Scanners.string("-").source()).optional().followedBy(whiteSpace());
    }

As seen in the code, we first built a whitespace parser, it scans for ” ” zero or many times, and returns the scanned whitespace(s) as a string(by using source()). I won’t talk much on the syntax and APIs since they are basically human readable and you can find pretty much you need in the documentation listed at the beginning of this article.

Later on, we used the similar way to generate more fundamental parsers such as parentheses, comma, and dot. Note how we generate these parsers by taking advantage of the whitespace parser we built earlier: each parentheses, comma and dot can comes after whitespace, and followed by more whitespace. We are now in the evolution!

Let’s together see a more complex case:

//decimal number with possible negative sign
    private static final Parser<Integer> integer() {
        return Parsers.sequence(negativeSign(), Scanners.INTEGER, new Map2<String, String, Integer>() {
            public Integer map(String neg, String value) {
                if (neg != null) {
                    return -Integer.parseInt(value);
                } else {
                    return Integer.parseInt(value);
                }
            }
        });
    }

In the above code, we generated a parser that parses negative integers. Whenever we see a negative sign followed by an integer, we create a map to translate these inputs to the result we want(in this case a negative integer). The map defines three parameter types, the first two comes from the parsers in the sequence. and the last parameter type is the result(return) type. Inside the map method, we can do whatever we want to generate the ideal result. In this case, if we see a negative sign, we return the negative value of the value we got from the second parser in the sequence, otherwise we return this value directly.

In the end, if we want to parse a string “-5”, we write:

int result = integer().parse("-5"); 

We got result = -5.

Here I’m only showing a basic example, but you can already see the power of Jparsec. You can use it to define your own gramma and parse whatever value you want from any kind of string input format. It’s much more human readable than regex, and it’s much more reliable than str.split().

Have fun with Jparsec!

Android: RecyclerView and its Custom LayoutManager

It has been a few months now since the RecyclerView was introduced by Google last year, and finally I got a chance to play with it. This article is mainly focused on building a custom LayoutManager for the RecyclerView. I’ll skip most of fundamental parts of RecyclerView because there are many such tutorials already available on the Internet.

Starting with basics:

Per Google’s definition, RecyclerView is “a flexible view for providing a limited window into a large data set”. In other words, It is a more advanced and flexible version of ListView, or more generally speaking, it’s a CollectionView that handles a collection of child views. Everything you do with ListView you can also use RecyclerView to replace, but with more flexible layout management and animation effects. If you haven’t learnt all these stuff, I suggest to take a look at this tutorial.

Similar to ListView, a RecyclerView requires a adapter to bind data to views. In this case, the adapter needs to extend RecyclerView.Adapter class. In addition, you need to set a LayoutManager to your RecyclerView for measuring and laying out all child views within the RecyclerView. This step seems to be extra work appending to ListView, but it gives you more power to handle the layout to look exact like the way you want it. The LayoutManager also needs to determine the right time to recycle a view when it’s not visible anymore. We will discuss this in the following sample.

A sample adapter implementation is already listed in the tutorial mentioned above, take a close look then you will realize it’s just like the adapter for ListView, but enforces the ViewHolder mechanism. It’s very clear to follow so I will skip this part.

By default, Android supports 3 LayoutManagers: LinearLayoutManager, GridLayoutManager, and StaggeredGridLayoutManager. What if you want something more flexible? You can implement your own LayoutManager by extending RecyclerView.LayoutManager. Next let’s walk through a sample custom LayoutManager that places child views at predefined locations, while recycle views that are no longer visible and re-use them when a same type of view need to be shown.

After you created your LayoutManager class, the only required override method is:

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT);
}

It basically creates a default LayoutParams for the RecyclerView. Now your LayoutManager is compilable!

Starting from here, things get more interesting. The next important method you probably want to override is:

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

}

this method is called when the child views are initially placed, and when the adapter’s dataset changes. In our sample case, we want it to layout all visible children.

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    fillVisibleChildren(recycler);
}

private void fillVisibleChildren(RecyclerView.Recycler recycler){
    //before we layout child views, we first scrap all current attached views
    detachAndScrapAttachedViews(recycler);

    //layoutInfo is a Rect[], each element contains coordinates for a view.
    for(int i = 0; i < layoutInfo.length; i++){
        if(isVisible(i)){
            View view = recycler.getViewForPosition(i);
            addView(view);
            layoutDecorated(view, layoutInfo[i].left, layoutInfo[i].top - verticalScrollOffset, layoutInfo[i].right, layoutInfo[i].bottom - verticalScrollOffset);
        }
    }
}

/*determine whether a child view is now visible
**getVerticalSpace() and getHorizontalSpace() returns layout space minus paddings.
**verticalScrollOffset and horizontalScrollOffset are current offset distances
**according to the initial left top corner, before any scrolling.
*/
private boolean isVisible(int index){
    if(layoutInfo[index].bottom < verticalScrollOffset
            || layoutInfo[index].top > getVerticalSpace() + verticalScrollOffset
            || layoutInfo[index].right < horizontalScrollOffset
            || layoutInfo[index].left > getHorizontalSpace() + horizontalScrollOffset){
        return false;
    }
    else{
        return true;
    }
}

At this point, you already have the initial layout setup, all visible child views are now on your screen!

The next step is to enable scrolling, this is a RecyclerView after all!

To enable scrolling, you need to override the following two methods, depending on your scroll direction:

@Override
public boolean canScrollHorizontally() {
    return true;
}

@Override
public boolean canScrollVertically() {
    return true;
}

This will give your layout scrolling ability, but we need to tell the LayoutManager what to change after we scroll. Let’s keep going, override the following methods:

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel;
    final int leftLimit = 0;
    final int rightLimit = findRightLimit(); //a helper method to find the rightmost child's right side.
    if(dx + horizontalScrollOffset < leftLimit){
        travel = horizontalScrollOffset;
        horizontalScrollOffset = leftLimit;
    }
    else if(dx + horizontalScrollOffset + getHorizontalSpace() > rightLimit){
        travel = rightLimit - horizontalScrollOffset - getHorizontalSpace();
        horizontalScrollOffset = rightLimit - getHorizontalSpace();
    }
    else{
        travel = dx;
        horizontalScrollOffset += dx;
    }
    fillVisibleChildren(recycler);
    return travel;
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {

    int travel;
    final int topLimit = 0;
    final int bottomLimit = findBottomLimit();//a helper method to find the bottommost child's bottom side.
    if(dy + verticalScrollOffset < topLimit){
        travel = verticalScrollOffset;
        verticalScrollOffset = topLimit;
    }
    else if(dy + verticalScrollOffset + getVerticalSpace() > bottomLimit){
        travel = bottomLimit - verticalScrollOffset - getVerticalSpace();
        verticalScrollOffset = bottomLimit - getVerticalSpace();
    }
    else{
        travel = dy;
        verticalScrollOffset += dy;
    }
    fillVisibleChildren(recycler);
    return travel;
}

The dx and dy parameter in these two methods are the distances to scroll in pixel. dx increases as scroll position approaches the right. dy increases as scroll position approaches the bottom. These two methods return the actual distance travelled. As boundary limitations, the returned distance may be smaller than dx or dy. We have to handle the boundary cases by ourselves, simple math though.

getVerticalSpace();
getHorizontalSpace();

returns the available space of the visible area, for example, a normal getVerticalSpace() implementation returns  getMeasuredHeight() – getPaddingTop() – getPaddingBottom().

verticalScrollOffset and horizontalScrollOffset are local variables that keep a record of the current scrolling offset. Every time you scroll the view these values get updated. These values are used when we calculate if a child view is currently visible. See

private boolean isVisible(int index)

above for example.

After we return the actual scrolled distance, we update locations for all child views, by calling fillVisibleChildren() again.

Now you have your first custom LayoutManager for RecyclerView. See how simple it is!

If you are interested in more complex cases: you can take a look at this blog, which introduce a custom grid LayoutManager.