Thursday, April 2, 2015

Fun with Parallax


Do you like fancy parallax effect?
Do you want to add it with ease to any view?

So do I. My first steps were to extend View and draw everything myself. Later I've tried to extend ImageView and play with its Matrix. But I always wanted an easier solution. Without changing each view that supposed to have one. And here what I came up with.



There is only one solution to support any view/hierarchy without changing it - we need custom layout that will add parallax effect to its content. Idea is following:


FrameLayout will emulate more space for its content. So for example if FrameLayout width is 100px it will position its content as if it had 150px. This extra space is required for parallax animation. This is how it can be done:
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(
        updateMeasureSpec(widthMeasureSpec),
        updateMeasureSpec(heightMeasureSpec)
    );

    int width = (int) (getMeasuredWidth() / (1 + 2 * mStrength));
    int height = (int) (getMeasuredHeight() / (1 + 2 * mStrength));

    setMeasuredDimension(width, height);
  }

  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    mOffsetX = (int) ((right - left) * mStrength);
    mOffsetY = (int) ((bottom - top) * mStrength);

    super.onLayout(changed, left - mOffsetX, top - mOffsetY, right + mOffsetX, bottom + mOffsetY);

    for (int index = getChildCount() - 1; index >= 0; --index) {
      View view = getChildAt(index);
      view.offsetLeftAndRight(-mOffsetX);
      view.offsetTopAndBottom(-mOffsetY);
    }
  }

  /*
   * if we have EXACTLY - emulate more space for measuring
   * if we have anything else - measure content and than decrease our size based on it
   */
  private int updateMeasureSpec(int measureSpec) {
    int mode = MeasureSpec.getMode(measureSpec);
    if (mode != MeasureSpec.EXACTLY) {
      return measureSpec;
    }
    int size = MeasureSpec.getSize(measureSpec);
    int offset = (int) (size * mStrength);
    return MeasureSpec.makeMeasureSpec(size + offset * 2, mode);
  }
Now we need to offset content. We can do this in scroll callback from ViewTreeObserver but I prefer to use pre-draw callbacks. This way offsets will be updated even during transition animations.
  @Override
  protected void onAttachedToWindow() {
    super.onAttachedToWindow();

    getViewTreeObserver().addOnPreDrawListener(this);
  }

  @Override
  protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();

    getViewTreeObserver().removeOnPreDrawListener(this);
  }

  @Override
  public boolean onPreDraw() {
    getLocationInWindow(mLocationCache);

    View root = getRootView();
    int width = getWidth();
    int height = getHeight();
    int rootWidth = root.getWidth();
    int rootHeight = root.getHeight();

    // parallax effect [0..1]
    float parallaxX = width < rootWidth ? mLocationCache[0] / (float) (rootWidth - width) : .5f;
    float parallaxY = height < rootHeight ? mLocationCache[1] / (float) (rootHeight - height) : .5f;

    // parallax offset [-value..+value]
    float offsetX = (parallaxX * 2 - 1f) * mOffsetX;
    float offsetY = (parallaxY * 2 - 1f) * mOffsetY;

    for (int index = getChildCount() - 1; index >= 0; --index) {
      View view = getChildAt(index);
      view.setTranslationX(-offsetX);
      view.setTranslationY(-offsetY);
    }

    return true;
  }
Thats it! Now all content inside our layout will have parallax effect.

Sources: https://github.com/MatrixDev/FunWithParallax