滑动内联动效的实现之图片平行逆差效果

本文属于滑动内联动效系列的第一篇。仓库地址

滑动内联动效 指的是 在容器滑动的过程中,其子View对应展现出来的一些效果。而图片平行逆差效果,就是在容器滑动过程中,图片也跟着移动的效果。语言太苍白,直接上效果。

《滑动内联动效的实现之图片平行逆差效果》 单张图片的item
《滑动内联动效的实现之图片平行逆差效果》 图文混排的逆差效果

上面图片还带了透明度的变化,但这不是本文的描述范围。
想要提前看整体实现,请直接移步到github仓库

图片平行逆差效果早见于网络,常见思路有两种:
1- 继承滑动容器或者在滑动容器的监听器里做文章
比如ScrollParallexListview..xxRecyclerview..xxParallex等命名的,github上比较好找。这类实现适用性比较单一,换种滑动容器的时候可能就会失效或者bug一堆。而且实现较为复杂,动效改动/添加会比较麻烦。

2- 自定义ImageView
这类实现也是比较常见的实现方式,其优点是可移植性高,在很多地方只要用这个ImageView即可实现平行逆差效果。但是这种方式也具有一些缺点,a-裁剪,这种方式具有天生的缺陷,即当ImageView最初设置layoutparams,在不改变固有比例的情况下,其很可能会被裁剪,具体裁剪规则参见ScaleType属性。b-适用范围小,只适用于图片,特别是有一些其它动画,如缩放和透明度变化时。

本文思路–包装容器(container)

熟悉ScrollBy方法的童鞋知道,其实所有的View都是可滑动的,只是滑动容器(比如ListView)滑动时,动的是子View,非滑动容器(TextView)滑动时,动的是其文本内容。总体来看,所有的view都可滑动,滑动时,动的都是其内容。由此得到灵感,将ImageView放到一个非滑动容器(container)中,那么ImageView将不会被裁剪,而平行逆差效果,却能由这个container的滑动来实现。这样做,既会保留自定义ImageView的高的移植性,又能避免图片被裁剪,而且容器不只滑动,它还能缩放,透明度或者旋转等等效果,使得动画的添加也很方便。
注意:包装容器不应该是常规的滑动容器。

方案分析:

  • 1 获得外面滑动容器的滑动事件。
    因为是做滑动内联效果,那么理应得到滑动事件才行。一般的滑动监听接口是不行了,因为我们要做的是兼容多种滑动容器。此时,我们选用的是ViewTreeObserver.OnScrollChangedListener,这接口非常通用,几乎所有可滑动视图体系都会引起它的调用。有接口了,什么时候注册接口呢,当然是view添加到window时啦,此时view的方法onAttachedToWindow开始发挥作用。

  • 2 得到滑动容器的位置范围。
    这个滑动容器可大可小,滑动内联效果肯定是与这个有关系的。假设有个点,刚好位于滑动容器的最下边。当滑动进行时,这个点便会跟着向下移动,当其到滑动容器最上边时,这个点刚好走了滑动容器的上下距离。这个过程,也代表了比较理想的内联动效的起始和最终位置。

  • 3 确定包装容器和图片的内联滑动
    滑动开始了,也知道什么时候内联滑动开始了,那么包装容器和图片应该怎么内联呢。用个图片来标示吧,直观。

《滑动内联动效的实现之图片平行逆差效果》 包装容器和图片的内联滑动

好了,方案分析完了。终于到上代码的时候了。

代码实现

  • 图片需要保持自身比例,而且不能被容器大小限制或者裁剪,那么这个ImageView就需要重写下测量方法。整体比较简单,就是设定了水平滑动或者纵向滑动。其宽高由滑动方向和图片固有的宽高决定。
public class AdjointImageView extends ImageView {
    private boolean isVertical = true;
    public AdjointImageView(Context context) {
        this(context, null);
    }
    public AdjointImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public AdjointImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AdjointContainer);
        isVertical = typedArray.getBoolean(R.styleable.AdjointContainer_isVertical, true);
        typedArray.recycle();
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getDrawable() == null) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        if (isVertical) {
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = width * getDrawable().getIntrinsicHeight() / getDrawable().getIntrinsicWidth();
            setMeasuredDimension(width, height);
        } else {
            int height = MeasureSpec.getSize(heightMeasureSpec);
            int width = height * getDrawable().getIntrinsicWidth() / getDrawable().getIntrinsicHeight();
            setMeasuredDimension(width, height);
        }
    }
}
  • 重点,包装容器的实现
public class AdjointContainer extends RelativeLayout implements ViewTreeObserver.OnScrollChangedListener {
    private boolean enableScrollParallax = true;
    private int[] viewLocation = new int[2];//自身位置
    //特效集合
    private List<AdjointStyle> mAdjointStyles = new ArrayList<>();
    //滑动容器的范围,矩形
    private Rect parentLocation = new Rect();//parent list rect
    //方便获得滑动容器范围
    private Locator mLocator;
    public AdjointContainer(Context context) {
        super(context);
        init();
    }
    public AdjointContainer(Context context,  AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public AdjointContainer(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        //为了使invalidate调用onDraw方法
        setBackgroundColor(0x0000);
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnScrollChangedListener(this);
    }
    @Override
    protected void onDetachedFromWindow() {
        getViewTreeObserver().removeOnScrollChangedListener(this);
        super.onDetachedFromWindow();
    }
    //增加动效  
    public void addStyle(AdjointStyle aAdjointStyle) {
        mAdjointStyles.add(aAdjointStyle);
    }
    public void removeStyle(AdjointStyle aAdjointStyle) {
        mAdjointStyles.remove(aAdjointStyle);
    }
    public void clearStyles(){
        mAdjointStyles.clear();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        if (mLocator != null) {
            parentLocation = mLocator.getLocation();
        }
        if (!enableScrollParallax || parentLocation==null||parentLocation.bottom == 0) {
            super.onDraw(canvas);
            return;
        }
        getLocationInWindow(viewLocation);
        for (int i = 0; i < mAdjointStyles.size(); i++) {
            mAdjointStyles.get(i).transform(this, canvas, viewLocation, parentLocation);
        }
        super.onDraw(canvas);
    }
    public void setLocator(Locator aLocator) {
        mLocator = aLocator;
    }
    @Override
    public void onScrollChanged() {
        if (enableScrollParallax) {
            invalidate();
            requestLayout();
        }
    }
}

容器做的工作主要有,接收滑动事件,确定滑动位置,增/删动效,通知动效对象执行动效。而动效对象的添加,是通过策略模式和观察者模式来实现。

  • 纵向平行逆差效果

public class VerticalMoveStyle implements AdjointStyle {
    @Override
    public void onAttachedToImageView(AdjointContainer view) {
    }
    @Override
    public void onDetachedFromImageView(AdjointContainer view) {
    }
    @Override
    public void transform(AdjointContainer aContainer, Canvas canvas, int[] viewLocation, Rect parentLocation) {
        if (aContainer.getChildCount() != 1) {
            return;
        }
        if (aContainer.getChildAt(0) instanceof AdjointImageView) {
            ALog.single().ld("transform-begin");
            AdjointImageView childView = (AdjointImageView) aContainer.getChildAt(0);
            Drawable drawable = (childView).getDrawable();
            int iWidth = drawable.getIntrinsicWidth();
            int iHeight = drawable.getIntrinsicHeight();
            int y = viewLocation[1];
            int ptop = parentLocation.top;
            int pbottom = parentLocation.bottom;
            ALog.single().ld("parentLocation.bottom--" + parentLocation.bottom);
            if (iWidth <= 0 || iHeight <= 0) {
                return;
            }
           int vWidth = aContainer.getWidth() - aContainer.getPaddingLeft() - aContainer.getPaddingRight();
            int vHeight = aContainer.getHeight() - aContainer.getPaddingTop() - aContainer.getPaddingBottom();
            int dHeight = ScreenUtil.getScreenHeight(aContainer.getContext());
            dHeight = dHeight < pbottom ? dHeight : pbottom;

            if (iWidth * vHeight < iHeight * vWidth || iHeight > vHeight) {
                // avoid over scroll
                if (y < ptop - vHeight) {
                    y = ptop - vHeight;
                } else if (y > dHeight) {
                    y = dHeight;
                }
                y = y - ptop;
                ALog.single().ld("target y:" + y);
                float imgScale = (float) vWidth / (float) iWidth;
                float imgMaxMoveScope = Math.abs((iHeight * imgScale - vHeight));
                int itemMaxMoveScope = pbottom - ptop - vHeight;
                float translateY = -(imgMaxMoveScope * y / itemMaxMoveScope);
                canvas.translate(0, translateY);
            }
        }
    }
}

这个动效的实现思路基本就是上面那个图片的体现。

到这个时候,一个可移植性比较高的滑动平行逆差效果就实现了,简单简洁。怎么使用呢,还是上代码吧,一种相当简易的使用,放到ScrollView中。

—-步骤 1

布局代码

...<sth like scrollview>...省略某些
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#66ffffff"
              android:orientation="horizontal"
              android:padding="10dp">
    <com.cysion.adjointlib.view.AdjointContainer
        android:id="@+id/container"
        android:layout_width="180dp"
        android:layout_height="100dp"
        android:layout_gravity="center_horizontal"
        android:layout_margin="10dp"
        >
        <com.cysion.adjointlib.view.AdjointImageView
            android:id="@+id/img_holder_img"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:adjustViewBounds="true"
            android:background="#99ff0000"
            android:padding="3dp"
            />
    </com.cysion.adjointlib.view.AdjointContainer>
    ...other view...
</LinearLayout>
...</sth like scrollview>...

—步骤 2

获得滑动容器的位置信息,以Rect标示,并提供一个Locator来传递给AdjointContainer.省略了一些,就是onCreate方法中获得滑动容器的位置,提供给包装容器。

public class SecondActivity extends AppCompatActivity implements Locator...
mContainer1 = (AdjointContainer) findViewById(R.id.adcontainer1);
..  {
        mScrollView.post(new Runnable() {
            @Override
            public void run() {
            mScrollView.getGlobalVisibleRect(mR);
            mContainer1.setLocator(SecondActivity.this);
        }
});
..  }
    @Override
    public Rect getLocation() {
        return mR;
    }

—步骤 3

创建AdjointStyle对象,并设置给容器。

 AdjointStyle style= new VerticalMoveStyle().minScale(0.9f);
 mContainer1.addStyle(style);

此时,滑动容器滑动时,图片也会滑动,产生逆差效果。

上面主要介绍了思路。完整例子见github仓库

    原文作者:CysionLiu
    原文地址: https://www.jianshu.com/p/28310233c841
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞