LZ-Says:与寒冬想比,更冷的是人心,更暖的同样为人心。。。
前几天,伙计推荐了有关平行空间效果,个人觉得很Nice,今天一起来研究,研究,看看如何实现。
跟着思路一起走,读完包你面对平行空间效果不再那么束手束脚~
我们先来看一下推荐效果:
Example 1: 平行空间
Example 2: 视差动画
以上俩种效果与之前开发中几张照片左右切换效果确实好了很多,值得我们一试~
我们就拿示例一进行分析,分析如下:
首先,我们需要明确的是,在滑动的过程中,哪儿些是可变因素,哪儿些是不变因素?
1. 可变因素:背景、手机内容;
2. 不变因素:手机外观不变、文字
可以确定的是,我们外部采用ViewPager进行包裹实现。
接着,我们来想一下布局,也就是父容器采用什么比较合理?
仔细对比原图后,我们可以采用"相对布局"or"帧布局"实现。
而其中效果又是如何实现的呢?
首先执行平移动画,随即布局完成随即进行翻转动画,最后在滑动的时候背景进行渐变,而这里动画我们采用属性动画来实现。
那么具体我们如何实现手机内容平移的呢?
思路1: 裁剪并拼接俩张图片
给我们的ImageView(手机)设置自定义Drawable,在滑动的时候,不断的将俩张图片进行裁剪并且拼接成一张照片显示即可。
下面简单放置几个简图便于理解:
默认进入首页时,显示红色部分图片,而蓝色部分图片则是看不见,也可以理解为蓝色部分图片处于隐藏状态。
当用户滑动屏幕,假设滑动X轴偏移量为屏幕一半时,通过裁剪图片并进行拼接从而形成下面黄色部分图片并进行显示。
这样,不乏是一种解决方法,那么还有其他的方法么?继续往下看。
思路2: HorizontalScrollView和俩张拼接在一块的图片实现
可以通过HorizontalScrollView和俩张拼接在一块的图片实现,那么这里的俩张拼接在一块的图片与上面拼接的图片有什么区别呢?
思路1中图片裁剪拼接而成最后显示的图片实际原图需要俩张图片,而思路2的方式只是将思路1的图片合并成一张,秒懂?
放置简图一张:
手机壳作为背景,中间内容图片采用HorizontalScorllView,且设置宽度与背景(手机)一样即可。那么如何保证与手机宽高一致呢?
采用百分比布局。相对屏幕的宽高设置自身的宽高比。
而谷歌官方提供的百分比布局,只能相对于屏幕宽度和高度进行设置,但是这样相对于我们这里,会造成图片变形,那么如何解决呢?
自定义百分比布局。使其可以支持依据屏幕宽度进行设置百分比,什么意思呢?看下面:
谷歌的百分比布局使用大致如下:
android:layout_width="50%w"
android:layout_height="40%h"
但是这种会造成图片拉伸,所以我们只能采用自定义百分比布局,效果如下:
android:layout_width="50%w"
android:layout_height="40%w"
思路简单到这里,下面会在撸码部分进行细致说明。
基于上面分析,我们着手搭建页面布局,很是Easy,所有内容都是在ViewPager里面完成,这里上面我们也说过,So,布局如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.materialdesignstudy.parallel.ParallelActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
RelativeLayout>
接着,我们先挑软柿子捏,定义HorizontalScrollView,里面主要部分就是对事件进行拦截。关键代码如下:
/**
* author : HLQ
* e-mail : [email protected]
* time : 2017/12/06
* desc : 自定义HorizontalScrollView 拦截事件处理避免滑动手机时,手动滑动手机内容时,内容随事件滑动
* version: 1.0
*/
public class MyScrollView extends HorizontalScrollView {
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent motionEvent) {
return true;
}
}
而关于谷歌百分比布局需要修改时,这块不做过多说明,到时候有需要直接拿过去用即可。
而这里介绍说明下布局该如何布。
首先放置一张简图,如下:
下面依据上面图例进行说明:
首先毋庸置疑的是根布局是我们自定义的百分比相对布局;
首先,放置我们的背景图,这里使用帧布局,是俩张图宽度填充屏幕,高度自适应且俩张图重叠。;
其次,依次摆放Icon,Title,Content,相对布局,很是easy;
之后,放置手机框背景图,设置百分比以及让其呈现在屏幕底部且居中;
最后,放置我们自定义ScorllView,并设置百分比,使其内容被放置在手机框内。
至此,布局效果完成后,效果如下:
布局源码如下:
<com.materialdesignstudy.parallel.support.PercentRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<FrameLayout
android:id="@+id/bg_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView0_2"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="centerCrop"
android:src="@drawable/pg1_2" />
<ImageView
android:id="@+id/imageView0"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="centerCrop"
android:src="@drawable/pg1_1" />
FrameLayout>
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="17dp"
android:src="@drawable/wechat1" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/imageView1"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:text="工作圈和生活圈\n互不干扰"
android:textColor="#fff"
android:textSize="30dp" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView1"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:text="通过平行空间同时运行两个微信账号,将你的工作和生活轻松分离"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#fff" />
<com.materialdesignstudy.parallel.MyScrollView
android:id="@+id/scrollview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:scrollbars="none"
app:layout_heightPercent="78%w"
app:layout_widthPercent="44%w">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="left"
android:orientation="horizontal">
<ImageView
android:id="@+id/pg1_content1"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:scaleType="fitStart"
android:src="@drawable/weixin" />
LinearLayout>
com.materialdesignstudy.parallel.MyScrollView>
<ImageView
android:id="@+id/iv_phone_bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="@drawable/phone1"
app:layout_heightPercent="88%w"
app:layout_widthPercent="63%w" />
com.materialdesignstudy.parallel.support.PercentRelativeLayout>
依次编写剩下的布局即可。
实例化Fragment,如下:
public class ParallelActivity extends FragmentActivity {
private ViewPager mViewPager;
private int[] mLayouts = {
R.layout.welcome1,
R.layout.welcome2,
R.layout.welcome3
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_parallel);
mViewPager = findViewById(R.id.viewpager);
WelcomePagerAdapter adapter = new WelcomePagerAdapter(getSupportFragmentManager());
mViewPager.setOffscreenPageLimit(3);
mViewPager.setAdapter(adapter);
}
class WelcomePagerAdapter extends FragmentPagerAdapter {
public WelcomePagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Fragment getItem(int position) {
Fragment fragment = new TranslateFragment();
Bundle bundle = new Bundle();
bundle.putInt("layoutId", mLayouts[position]);
bundle.putInt("pageIndex", position);
fragment.setArguments(bundle);
return fragment;
}
@Override
public int getCount() {
return mLayouts.length;
}
}
}
接下来,我们需要需要思考下,这些效果是在什么时候会被触发的呢?
1. 启动时,也就是进入时会执行平移动画以及背景颜色渐变;
2. 切换页面,也就是滑动时,会进行翻转动画、背景颜色渐变以及平移动画。
那么基于以上,我们定义一个WelcomePagerTransformer管理类,相关操作都将会在这里进行,首先为Activity添加事件处理。
mTransformer = new WelcomePagerTransformer();
// 监听滑动时,当前View在ViewPager中的Index 也可以进行动画效果设置 例如3D翻转等
mViewPager.setPageTransformer(true, mTransformer);
// 监听当前滑动的页面方向
mViewPager.setOnPageChangeListener(mTransformer);
而关于上面俩个事件,LZ找了点说明,地址会在文末附上:
ViewPager:
viewpager有一个setPageTransformer(),主要是用在viewpager在滑动时的回调,第一个参数一般为true。它的参数为PageTransformer变量。
PageTransformer 参考
它是一个接口,里面的方法只有一个transformPage(View view,float position)。参数含义分别是:当前的view,以及该view在viewpager中的位置。不同的view,position的值是不同的。
停止滑动后,显示在屏幕中的view的position为0。往左滑时,该view的position会随着滑动的移动而逐渐降低到-1;往后滑时,position会随着滑动逐渐增加到1。position的值大于1,或者小于-1,那么该position已经不可见。
viewpager会保留当前屏幕,左边及右边一共3个view。超出1,那就代表该view是右边往右,小于-1就代表该view是左边往左。
接下来,让我们一起来编写真正的管理实现类: WelcomePagerTransformer
public class WelcomePagerTransformer implements ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
// 定义滑动角度
private static final float ROT_MOD = -15f;
private int mPageIndex;
private boolean mPageChanged = true;
/**
* 此方法是滑动的时候每一个页面View都会调用该方法
* view:当前的页面
* position:当前滑动的位置
* 视差效果:在View正常滑动的情况下,给当前View或者当前view里面的每一个子view来一个加速度
* 而且每一个加速度大小不一样。
*/
@Override
public void transformPage(View view, float position) {
// 实例化
ViewGroup v = view.findViewById(R.id.rl);
final MyScrollView scrollView = v.findViewById(R.id.scrollview);
View bg1 = v.findViewById(R.id.imageView0);
View bg2 = v.findViewById(R.id.imageView0_2);
View bg_container = v.findViewById(R.id.bg_container);
// 获取颜色值
int bg1_green = view.getContext().getResources().getColor(R.color.bg1_green);
int bg2_blue = view.getContext().getResources().getColor(R.color.bg2_blue);
// 获取Tag 找到当前的位置以及View
Integer tag = (Integer) view.getTag();
View parent = (View) view.getParent();
// 颜色估值器 颜色渐变类
ArgbEvaluator evaluator = new ArgbEvaluator();
int color = bg1_green;
if (tag.intValue() == mPageIndex) {
switch (mPageIndex) {
case 0:
color = (int) evaluator.evaluate(Math.abs(position), bg1_green, bg2_blue);
break;
case 1:
color = (int) evaluator.evaluate(Math.abs(position), bg2_blue, bg1_green);
break;
case 2:
color = (int) evaluator.evaluate(Math.abs(position), bg1_green, bg2_blue);
break;
default:
break;
}
// 设置整个viewpager的背景颜色
parent.setBackgroundColor(color);
}
if (position == 0) {
// pageChanged作用--解决问题:只有在切换页面的时候才展示平移动画,如果不判断则会只是移动一点点当前页面松开也会执行一次平移动画
if (mPageChanged) {
bg1.setVisibility(View.VISIBLE);
bg2.setVisibility(View.VISIBLE);
ObjectAnimator animator_bg1 = ObjectAnimator.ofFloat(bg1, "translationX", 0, -bg1.getWidth());
animator_bg1.setDuration(400);
animator_bg1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scrollView.smoothScrollTo((int) (scrollView.getWidth() * animation.getAnimatedFraction()), 0);
}
});
animator_bg1.start();
ObjectAnimator animator_bg2 = ObjectAnimator.ofFloat(bg2, "translationX", bg2.getWidth(), 0);
animator_bg2.setDuration(400);
animator_bg2.start();
mPageChanged = false;
}
} else if (position == -1 || position == 1) { // 效果回复默认
bg2.setTranslationX(0);
bg1.setTranslationX(0);
scrollView.smoothScrollTo(0, 0);
} else if (position < 1 && position > -1) {
final float width = bg1.getWidth();
final float height = bg1.getHeight();
final float rotation = ROT_MOD * position * -1.25f;
// 这里不去分别处理bg1、bg2,而是用包裹的父容器执行动画,目的是避免难以处理两个bg的属性位置恢复。
bg_container.setPivotX(width * 0.5f);
bg_container.setPivotY(height);
bg_container.setRotation(rotation);
}
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
mPageIndex = position;
mPageChanged = true;
}
@Override
public void onPageScrollStateChanged(int state) {
}
}
源码如上,下面进行关键点分析:
首先,transformPage方法便是滑动过程中每个页面的View都会调用该方法,我们在这里面进行实例化控件、获取颜色值、获取Tag以及设置颜色估值器(此类作用就是根据传入的颜色进行位移,从而得到颜色相近的颜色值);
接着,获取到当前的View在ViewPager的位置,从而开始动画展示。这里我们使用了一个标识符,也就是代表只有在切换页面的时候才进行动画效果,如果不添加此标识符则会出现当用户移动偏移量很小的时候,页面也会执行一次动画;
其次,通过属性动画ObjectAnimator来进行动画设置。而ObjectAnimator.ofFloat(bg1, "translationX", 0, -bg1.getWidth())则代表背景X轴进行平移也就是上来我们看到背景平移出去的一个效果
https://github.com/HLQ-Struggle/MaterialDesignStudy
如感觉有所收获,不妨赞助LZ喝点小玩意,一分也是爱~