I'm writing a custom ViewGroup to add a header for ViewPager (like Google+ app with tab swipe), and I found a problem. Child view inside this ViewGroup doesn't re-layout after size change.
What I expect:
What I really get:
The view hierarchy is
+ UserProfileDrawer extends ViewGroup
|
+--+ UserProfileDrawer$InternalContainer extends ViewGroup
|
+--+ Header view
|
+--+ Content view
Source code of the custom layout:
package org.mariotaku.userprofiledrawer;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by mariotaku on 14/11/27.
*/
public class UserProfileDrawerSimplified extends ViewGroup {
static final String LOGTAG = "UserProfileDrawer";
private final ViewDragHelper mDragHelper;
private final InternalContainer mContainer;
private DrawerCallback mDrawerCallback;
public UserProfileDrawerSimplified(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UserProfileDrawer);
final int headerLayoutId = a.getResourceId(R.styleable.UserProfileDrawer_headerLayout, 0);
final int contentLayoutId = a.getResourceId(R.styleable.UserProfileDrawer_contentLayout, 0);
addView(mContainer = new InternalContainer(this, context, headerLayoutId, contentLayoutId),
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
a.recycle();
mDragHelper = ViewDragHelper.create(this, new DragCallback(this));
}
public UserProfileDrawerSimplified(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public UserProfileDrawerSimplified(Context context) {
this(context, null);
}
@Override
public void computeScroll() {
boolean invalidate = mDragHelper.continueSettling(true);
if (invalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onFinishInflate() {
if (getChildCount() != 1) {
throw new IllegalArgumentException("Add subview by XML is not allowed.");
}
}
public void setDrawerCallback(DrawerCallback callback) {
mDrawerCallback = callback;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, j = getChildCount(); i < j; i++) {
final View child = getChildAt(i);
final int left = child.getLeft() + getPaddingLeft(), right = left + child.getMeasuredWidth() - getPaddingRight();
final int top = (i == 0 ? child.getTop() : getChildAt(i - 1).getBottom()) + getPaddingTop();
final int bottom = top + child.getMeasuredHeight() - getPaddingBottom();
child.layout(left, top, right, bottom);
}
}
public View getHeader() {
return mContainer.getHeader();
}
public View getContent() {
return mContainer.getContent();
}
private int getScrollRange() {
return mContainer.getScrollRange();
}
public int getHeaderTop() {
return mContainer.getTop();
}
public int getHeaderTopMaximum() {
return mContainer.getHeaderTopMaximum();
}
public int getHeaderTopMinimum() {
return mContainer.getHeaderTopMinimum();
}
private void notifyOffsetChanged() {
mDrawerCallback.topChanged(getHeaderTop());
}
public static interface DrawerCallback {
void topChanged(int offset);
}
private static class DragCallback extends ViewDragHelper.Callback {
private final UserProfileDrawerSimplified mDrawer;
public DragCallback(UserProfileDrawerSimplified drawer) {
mDrawer = drawer;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
mDrawer.mDragHelper.flingCapturedView(mDrawer.getPaddingLeft(),
mDrawer.getHeaderTopMinimum(), mDrawer.getPaddingLeft(),
mDrawer.getHeaderTopMaximum());
ViewCompat.postInvalidateOnAnimation(mDrawer);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDrawer.getScrollRange();
}
@Override
public boolean tryCaptureView(View view, int pointerId) {
return view == mDrawer.mContainer;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return mDrawer.getPaddingLeft();
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int min = mDrawer.getHeaderTopMinimum(), max = mDrawer.getHeaderTopMaximum();
return MathUtils.clamp(top, min, max);
}
}
@SuppressLint("ViewConstructor")
private static class InternalContainer extends ViewGroup {
private final UserProfileDrawerSimplified mParent;
private final View mHeaderView, mContentView;
public InternalContainer(UserProfileDrawerSimplified parent, Context context, int headerLayoutId, int contentLayoutId) {
super(context);
mParent = parent;
final LayoutInflater inflater = LayoutInflater.from(context);
addView(mHeaderView = inflater.inflate(headerLayoutId, this, false));
addView(mContentView = inflater.inflate(contentLayoutId, this, false));
}
public View getHeader() {
return mHeaderView;
}
public View getContent() {
return mContentView;
}
public int getHeaderTopMinimum() {
return mParent.getPaddingTop() - mHeaderView.getHeight();
}
public int getHeaderTopMaximum() {
return mParent.getPaddingTop();
}
public int getScrollRange() {
return getHeaderTopMaximum() - getHeaderTopMinimum();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, j = getChildCount(); i < j; i++) {
final View child = getChildAt(i);
final int left = child.getLeft(), right = left + child.getMeasuredWidth();
final int top = i == 0 ? child.getTop() : getChildAt(i - 1).getBottom();
final int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
}
}
@Override
public void offsetTopAndBottom(int offset) {
super.offsetTopAndBottom(offset);
mParent.notifyOffsetChanged();
}
}
}