Pseudo Selectors

Tags

, , , , , , , , , , , , ,

It’s 25 years already! Happy birthday, WWW ;) Let’s take a concept from HTML UI and try to achieve it in Android. CSS provides “pseudo selectors” that allows selecting nodes based on a particular state. The most commonly used one is :hover state for links. In terms of styling, especially when it involves rounded corners, the :first-child and the :last-child allows us to select the first and the last elements in a group. Wouldn’t life be easier if we had similar functionality in Android?

puedo-selectors

Android provides state_first and state_last that could be used with StateListDrawable. Though they aren’t inherently supported by ViewGroups, they are a good starting point. Let’s build a background for an element in the top horizontal row (above) with these states.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- First and last -->
    <item android:state_first="true"
          android:state_last="true">
    
        <shape>
            <solid android:color="#63abed"/>
            <corners android:radius="10dp"/>
        </shape>
        
    </item>
    
    <!-- First -->
    <item android:state_first="true">
    
        <shape>
            <solid android:color="#63abed"/>
            <corners android:topLeftRadius="10dp"
                     android:bottomLeftRadius="10dp"/>
        </shape>
        
    </item>
    
    <!-- Last -->
    <item android:state_last="true">
    
        <shape>
            <solid android:color="#63abed"/>
            <corners android:topRightRadius="10dp"
                     android:bottomRightRadius="10dp"/>
        </shape>
        
    </item>
    
    <!-- Default -->
    <item>
    
        <shape>
            <solid android:color="#63abed"/>
        </shape>
        
    </item>
        
</selector>

There are 4 cases — this could be the only element in its parent, or be the first, or the last, or anywhere in-between. The above selector takes care of all those states. As said earlier, Android doesn’t use these states. But we could easily override onCreateDrawableState() to achieve it.

    public class PseudoAwareTextView extends TextView {
        
        private static final int[] STATE_ONLY_ONE = new int[] {
            android.R.attr.state_first,
            android.R.attr.state_last,
        };
        
        private static final int[] STATE_FIRST = new int[] {
            android.R.attr.state_first
        };
        
        private static final int[] STATE_LAST = new int[] {
            android.R.attr.state_last
        };
    
        public PseudoAwareTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        
        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            ViewGroup parent = (ViewGroup) getParent();
            if (parent == null) {
                return super.onCreateDrawableState(extraSpace);
            }
            
            final int size = parent.getChildCount();
            final boolean isFirst = (parent.getChildAt(0) == this);
            final boolean isLast = (parent.getChildAt(size-1) == this);
            
            int[] states = super.onCreateDrawableState(extraSpace + 2);
            if (isFirst && isLast) {
                mergeDrawableStates(states, STATE_ONLY_ONE);
            } else if (isFirst) {
                mergeDrawableStates(states, STATE_FIRST);
            } else if (isLast) {
                mergeDrawableStates(states, STATE_LAST);
            }
            
            return states;
        }
    
    }

Since we know that this element is inside some ViewGroup, we can find the order of the element and set the states accordingly! Now, even if the app demands showing only one of the three elements, the background works like a charm! We don’t need booleans to keep track of what is visible and which of the four possible backgrounds an element should have.

How do we extend this to ListViews? They might seem tricky. But they follow similar logic. The background for a row is drawn by ListView, as specified in android:listSelector. (For brevity of this article, let us assume that we have such a selector in place — that is pretty similar to the one above, but has android:state_pressed=”true” in them). And since the listSelector is a Drawable, the owner ListView takes care of setting the proper state. Every time, before a selector is drawn over a row, the selector’s bounds are computed and the states are set. All we need to know at this point is the row, that is currently under the thumb, that needs a pressed state to be shown. That can be computed by storing the (x, y) co-ordinates of the TOUCH_DOWN event and finding the position using pointToPosition() method.

    public class PseudoAwareListView extends ListView {
        
        private static final int[] STATE_ONLY_ONE = new int[] {
            android.R.attr.state_first,
            android.R.attr.state_last,
        };
        
        private static final int[] STATE_FIRST = new int[] {
            android.R.attr.state_first
        };
        
        private static final int[] STATE_LAST = new int[] {
            android.R.attr.state_last
        };
        
        private int mTouchX, mTouchY;
    
        public PseudoAwareListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                mTouchX = (int) event.getX();
                mTouchY = (int) event.getY();
            } else {
                mTouchX = -1;
                mTouchY = -1;
            }
            
            return super.onInterceptTouchEvent(event);
        }
        
        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            if (!isPressed()) {
                return super.onCreateDrawableState(extraSpace);
            }
            
            final int selection = pointToPosition(mTouchX, mTouchY);
            final int size = getChildCount();
            final boolean isFirst = (0 == selection);
            final boolean isLast = (size-1 == selection);
            
            int[] states = super.onCreateDrawableState(extraSpace + 2);
            if (isFirst && isLast) {
                mergeDrawableStates(states, STATE_ONLY_ONE);
            } else if (isFirst) {
                mergeDrawableStates(states, STATE_FIRST);
            } else if (isLast) {
                mergeDrawableStates(states, STATE_LAST);
            }
            
            return states;
        }
    
    }

Note: The same effect on ListViews can be achieved by enclosing it inside a FrameLayout and drawing a foreground on top of it. That would be suitable for most cases. This is recommended if your ListView with rounded corners is over a layout with translucent background.

That was easy! Now we can do even more tricks like in the CSS — nth-child, optional, etc. Here’s to the next 25 years!

Follow

Get every new post delivered to your Inbox.

Join 578 other followers