Search code examples
javaandroidviewgroupviewdraghelper

Children match_parent ignoring parent in custom view with viewdraghelper


I have been studying custom views recently and trying to make one specific example work out from a tutorial located here: http://flavienlaurent.com/blog/2013/08/28/each-navigation-drawer-hides-a-viewdraghelper/

I will add the relevant code to prevent information loss from dead link. The author of the tutorial explains how to create a ViewDragHelper similar to YouTube's android app player behavior and these are the codes he provided

activity_main.xml

<FrameLayout

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

<ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:tag="list"
        />

<com.example.vdh.YoutubeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/youtubeLayout"
        android:orientation="vertical"
        android:visibility="visible">

    <TextView
            android:id="@+id/viewHeader"
            android:layout_width="match_parent"
            android:layout_height="128dp"
            android:fontFamily="sans-serif-thin"
            android:textSize="25sp"
            android:tag="text"
            android:gravity="center"
            android:textColor="@android:color/white"
            android:background="#AD78CC"/>

    <TextView
            android:id="@+id/viewDesc"
            android:tag="desc"
            android:textSize="35sp"
            android:gravity="center"
            android:text="Loreum Loreum"
            android:textColor="@android:color/white"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FF00FF"/>

</com.example.vdh.YoutubeLayout>

YoutubeLayout.java

public class YoutubeLayout extends ViewGroup {

    private final ViewDragHelper mDragHelper;

    private View mHeaderView;
    private View mDescView;

    private float mInitialMotionX;
    private float mInitialMotionY;

    private int mDragRange;
    private int mTop;
    private float mDragOffset;


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

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

    @Override
    protected void onFinishInflate() {
        mHeaderView = findViewById(R.id.viewHeader);
        mDescView = findViewById(R.id.viewDesc);
    }

    public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
    }

    public void maximize() {
        smoothSlideTo(0f);
    }

    boolean smoothSlideTo(float slideOffset) {
        final int topBound = getPaddingTop();
        int y = (int) (topBound + slideOffset * mDragRange);

        if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
            ViewCompat.postInvalidateOnAnimation(this);
            return true;
        }
        return false;
    }

    private class DragHelperCallback extends ViewDragHelper.Callback {

      @Override
      public boolean tryCaptureView(View child, int pointerId) {
            return child == mHeaderView;
      }

        @Override
      public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
          mTop = top;

          mDragOffset = (float) top / mDragRange;

            mHeaderView.setPivotX(mHeaderView.getWidth());
            mHeaderView.setPivotY(mHeaderView.getHeight());
            mHeaderView.setScaleX(1 - mDragOffset / 2);
            mHeaderView.setScaleY(1 - mDragOffset / 2);

            mDescView.setAlpha(1 - mDragOffset);

            requestLayout();
      }

      @Override
      public void onViewReleased(View releasedChild, float xvel, float yvel) {
          int top = getPaddingTop();
          if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
              top += mDragRange;
          }
          mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
      }

      @Override
      public int getViewVerticalDragRange(View child) {
          return mDragRange;
      }

      @Override
      public int clampViewPositionVertical(View child, int top, int dy) {
          final int topBound = getPaddingTop();
          final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();

          final int newTop = Math.min(Math.max(top, topBound), bottomBound);
          return newTop;
      }

    }

    @Override
    public void computeScroll() {
      if (mDragHelper.continueSettling(true)) {
          ViewCompat.postInvalidateOnAnimation(this);
      }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
      final int action = MotionEventCompat.getActionMasked(ev);

      if (( action != MotionEvent.ACTION_DOWN)) {
          mDragHelper.cancel();
          return super.onInterceptTouchEvent(ev);
      }

      if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
          mDragHelper.cancel();
          return false;
      }

      final float x = ev.getX();
      final float y = ev.getY();
      boolean interceptTap = false;

      switch (action) {
          case MotionEvent.ACTION_DOWN: {
              mInitialMotionX = x;
              mInitialMotionY = y;
                interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
              break;
          }

          case MotionEvent.ACTION_MOVE: {
              final float adx = Math.abs(x - mInitialMotionX);
              final float ady = Math.abs(y - mInitialMotionY);
              final int slop = mDragHelper.getTouchSlop();
              if (ady > slop && adx > ady) {
                  mDragHelper.cancel();
                  return false;
              }
          }
      }

      return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
      mDragHelper.processTouchEvent(ev);

      final int action = ev.getAction();
        final float x = ev.getX();
        final float y = ev.getY();

        boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
        switch (action & MotionEventCompat.ACTION_MASK) {
          case MotionEvent.ACTION_DOWN: {
              mInitialMotionX = x;
              mInitialMotionY = y;
              break;
          }

          case MotionEvent.ACTION_UP: {
              final float dx = x - mInitialMotionX;
              final float dy = y - mInitialMotionY;
              final int slop = mDragHelper.getTouchSlop();
              if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
                  if (mDragOffset == 0) {
                      smoothSlideTo(1f);
                  } else {
                      smoothSlideTo(0f);
                  }
              }
              break;
          }
      }


      return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
    }


    private boolean isViewHit(View view, int x, int y) {
        int[] viewLocation = new int[2];
        view.getLocationOnScreen(viewLocation);
        int[] parentLocation = new int[2];
        this.getLocationOnScreen(parentLocation);
        int screenX = parentLocation[0] + x;
        int screenY = parentLocation[1] + y;
        return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
                screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
        int maxHeight = MeasureSpec.getSize(heightMeasureSpec);

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
                resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      mDragRange = getHeight() - mHeaderView.getHeight();

        mHeaderView.layout(
                0,
                mTop,
                r,
                mTop + mHeaderView.getMeasuredHeight());

        mDescView.layout(
                0,
                mTop + mHeaderView.getMeasuredHeight(),
                r,
                mTop  + b);
    }
}

The author notes that onLayoutand onMeasure are badly written and I assume these (or one of them) might be the cause of a problem with one of the children.

For my objective, I replaced the mDescView with a Framelayout view containing the respective TextView. Both of them have their height set to match_parent and the parent (mDescView) does set its height correctly, but its children (TextView inside mDescView) ignore the parent height and stretch to fit their height equal to the screen height (or the custom view height, can't tell the difference). This is a problem because the mDescView children will never adjust their height correctly according to the parent through match_parent and I have been looking for a solution for days, but none was found and through research I couldn't find a reason for why this was happening.

This is the result of this problem, notice how the TextView height is bigger than its parent mDescView despite both having their height set to match_parent

enter image description here

So my problem is, how can I get the child (children) of that parent (or any parents) to match their parents' height like they should.

As an additional request, if possible, can anyone explain why the author thinks some of his methods are not the best/right ones and how they should be done correctly/better instead.


Solution

  • I have finally corrected this problem, although I am still unaware if this is the right way to do this. My solution was to set both views' height during onLayout I have also set this to only run on the first time this method is called, so the specific statement only runs once and only during the first time (when firstRun is true)

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mDragRange = getHeight() - mHeaderView.getHeight();
    
        if (firstRun) {
            firstRun = false;
            mDescView.getLayoutParams().height = getHeight() - mHeaderView.getMeasuredHeight();
        }
    
        mHeaderView.layout(
                0,
                mTop,
                r,
                mTop + mHeaderView.getMeasuredHeight());
    
        mDescView.layout(
                0,
                mTop + mHeaderView.getMeasuredHeight(),
                r,
                mTop  + b);
    }