前言
对于ViewPager,相信大家都已经很熟悉了,在各种切换场景比如Fragment切换、选项卡的切换或者顶部轮播图片等都可以用ViewPager去实现。那么本篇文章带来ViewPager的一种实现效果:3D画廊。直接上图来看:
ic.gif
从上面的图我们可以看出,整个页面分成三个部分,中间的是大图,正中地显示给用户;而两边的是侧图,而这两幅图片又有着角度的旋转,与大图看起来不在同一平面上,这就形成了3D效果。接着拖动页面,侧面的图慢慢移到中间,这个过程也是有着动画的,包括了图片的旋转、缩放和平移。在欣赏了上面的效果后,话不多说,我们来看看是怎样实现的。
实现原理
1、利用ViewGroup的clipChildren属性。大家可能对ClipChildren属性比较陌生,我们先来看看官方文档对该属性的描述:
Defines whether a child is limited to draw inside of its bounds or not. This is useful with animations that scale the size of the children to more than 100% for instance. In such a case, this property should be set to false to allow the children to draw outside of their bounds. The default value of this property is true.
上面的大意是说,ViewGroup的子View默认是不会绘制边界意外的部分的,倘若将clipChildren属性设置为false,那么子View会把自身边界之外的部分绘制出来。
那么这个属性跟我们的ViewPager又有什么关联呢?我们可以这样想,ViewPager自身是一个ViewGroup,如果将它的宽度限制为某一个大小比如200dp(我们通常是match_parent),这样ViewPager的绘制区域就被限制在了240dp内(此时绘制的是ViewA),此时我们将它的父容器的clipChildren属性设置为false,那么ViewPager未绘制的部分就会在两旁得到绘制(此时绘制的是ViewA左右两边的Item View)。
那么我们的布局文件可以这样写,activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="240dp"
android:layout_height="match_parent"
android:clipChildren="false"
android:layout_centerInParent="true">
</android.support.v4.view.ViewPager>
</RelativeLayout>
接着,我们需要为每个Item创建一个布局,这个很简单,就是一个ImageView,新建item_main.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv"
android:layout_width="240dp"
android:layout_height="360dp"
android:layout_centerInParent="true"/>
</RelativeLayout>
布局文件写好后,我们接着完成MainActivity.java和MyPagerAdapter.java的内容:
MainActivity.java:
public class MainActivity extends AppCompatActivity {
//这里的图片从百度图片中下载,图片规格是960*640
private static final int[] drawableIds = new int[]{R.mipmap.ic_01,R.mipmap.ic_02,R.mipmap.ic_03,
R.mipmap.ic_04,R.mipmap.ic_05,R.mipmap.ic_06,R.mipmap.ic_07,R.mipmap.ic_08,R.mipmap.ic_09,
R.mipmap.ic_10,R.mipmap.ic_11,R.mipmap.ic_12};
private ViewPager mViewPager;
private RelativeLayout mRelativeLayout;
private MyPagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
mViewPager = (ViewPager) findViewById(R.id.viewpager);
mPagerAdapter = new MyPagerAdapter(drawableIds,this);
mViewPager.setAdapter(mPagerAdapter);
}
}
MyPagerAdapter.java:
public class MyPagerAdapter extends PagerAdapter {
private int[] mBitmapIds;
private Context mContext;
public MyPagerAdapter(int[] data,Context context){
mBitmapIds = data;
mContext = context;
}
@Override
public int getCount() {
return mBitmapIds.length;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_main,container,false);
ImageView imageView = (ImageView) view.findViewById(R.id.iv);
imageView.setImageResource(mBitmapIds[position]);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}
ok,到现在为止,我们先运行一下看看结果如何:
ic_01.png
从上图可以看出,本来ViewPager设置的宽度是240dp,那么原来应该只会显示一个Page的内容,但是由于clipChildren=false属性的生效,使得ViewPager早240dp之外的部分也被绘制了出来。那么到目前为止,就实现了在一屏显示多个Page的效果了,那么接下来的3D效果怎样实现呢?
2、利用ViewPager.PageTransformer实现滑动动画效果
PageTransformer是Android3.0之后加入的一个接口,通过该接口我们可以方便地为ViewPager添加滑动动画,但是该接口只能用于Android3.0之后的版本,3.0之前的版本会被忽略。我们看看这个接口需要重写的唯一一个方法:
/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
*
* <p>As property animation is only supported as of Android 3.0 and forward,
* setting a PageTransformer on a ViewPager on earlier platform versions will
* be ignored.</p>
*/
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
void transformPage(View page, float position);
}
通过官方的注释,我们可以获得如下信息:①PageTransformer在可见Item或者被添加到ViewPager的Item的位置发生改变的时候,就会回调该方法。可见Item很容易理解,就是当前被选中的Page,那么attached page怎样理解呢?我们知道,ViewPager有着预加载机制,默认的预加载数量是1,即中心Item向左的一个Item以及向右的一个Item,由于预加载机制的存在使得ViewPager在滑动的过程中不会感到卡顿,因为需要展示的页面已经提前准备好了。
②关注transformPage(page,position)的方法参数,这里的position是存在一个范围的,0代表当前被选中的Page的位置,位于中心,如果当前Page向左滑动,那么position会从0减到-1,当Page向右滑动,position会从0增加到1。当一个page的position变为-1的时候,这个page便位于中心Item的左边了,相对的,position变成1的时候,这个page便位于中心Item的右边。利用这个position变化的性质,我们可以很轻松地对View的某些属性进行改变了。
接下来,新建RotationPageTransformer.java文件:
public class RotationPageTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE=0.85f;
@Override
public void transformPage(View page, float position) {
float scaleFactor = Math.max(MIN_SCALE,1 - Math.abs(position));
float rotate = 10 * Math.abs(position);
//position小于等于1的时候,代表page已经位于中心item的最左边,
//此时设置为最小的缩放率以及最大的旋转度数
if (position <= -1){
page.setScaleX(MIN_SCALE);
page.setScaleY(MIN_SCALE);
page.setRotationY(rotate);
}//position从0变化到-1,page逐渐向左滑动
else if (position < 0){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(rotate);
}//position从0变化到1,page逐渐向右滑动
else if (position >=0 && position < 1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(-rotate);
}//position大于等于1的时候,代表page已经位于中心item的最右边
else if (position >= 1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(-rotate);
}
}
}
接着,我们为ViewPager设置这样一个属性即可:
mViewPager.setPageTransformer(true,new RotationPageTransformer());
mViewPager.setOffscreenPageLimit(2); //下面会说到
我们运行一下代码,会发现结果跟最上面展示的效果图是一样的,此时滑动ViewPager,各个Item之间的切换也会有动画的出现,呈现出了3D效果。
3、setPageMargin(int)方法,PageMargin属性用于设置两个Page之间的距离,有需要的可以加上该属性,使得两个Page的区分更加明显。
4、setOffscreenPageLimit(int)方法,OffscreenPageLimit属性用于设置预加载的数量,比如说这里设置了2,那么就会预加载中心item左边两个Item和右边两个Item。那么这里这个属性对于我们的3D效果有什么影响呢?我们来试验一下,首先调用mViewPager.setOffscreenPageLimit(1),把预加载数量设置为1,然后运行程序,向左右滑动几次,会发现出现了下面的问题:
即左边或者右边的Item在滑动的过程中有可能出现不正确的显示,这是为什么呢?其实这是预加载的数量的问题,当前如果处于position为0的情况下,此时已经预加载了position为1的Item,那么该Item能正常显示,然而当滑动的时候,由于ViewPager是停止滑动的时候才会加载需要的Item,导致滑动到item1的时候,已经没有需要显示的Item2了(因此此时尚未加载),但是当手指松开的时候,Item2得到加载,但是此时不再调用transformPage()方法来调整自身的显示,所以造成了上面的错误显示。解决的办法是可以把预加载的数量设置为2或者3,这样得到的效果更好。
优化
在实现以上效果后,我们需要重新审视一遍我们的代码,看看是否还有优化的空间。
1、我们在Adapter中的instantiateItem()方法内加载一个View,并用了ImageView的setImageResource()方法来加载图片,其实查看该方法的源码可知,这个方法是在UI线程内加载图片的,如果加载的是很大的一张图片,那么就造成了UI线程的拥堵。
2、对于已经加载的图片,没有得到充分的利用,而是每次都加载一次,而旧的图片由于失去了引用又处于待回收的状态,这样不断的加载和回收无疑是加重了系统的负担。
3、如果ImageView的宽高小于图片的规格,那么把完整的一个大图加载到ImageView内,显然也是不合适的。因为图片越大的话,其占用的内存也越大。
针对上述所说的情况,我们可以一一找到对应的解决办法:
1、对于在UI线程加载图片的情况,我们可以考虑在子线程加载图片,等图片加载完毕后在通知主线程把图片设置进ImageView内即可。自然我们会想到使用Handler来进行线程之间的通信。但是这又引发一个问题,如果每一次的instantiateItem()方法内我们都新开一条线程去加载图片,那么最终的结果是创建了很多只用了一次的线程,这样的开销更大了。那有没有可以控制子线程的方法呢?答案是线程池。线程池通过合理调度线程的使用,使得线程达到最大的使用效率。那么我们可以直接使用AsyncTask来实现以上功能,因为AsyncTask内部也用到了线程池。
我们在MyPagerAdapter.java内新建一个内部类:
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
private ImageView imageView;
public LoadBitmapTask(ImageView imageView){
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(Integer... params) {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0]);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}
然后在instantiateItem()方法内添加如下代码:new LoadBitmapTask(imageView).execute(mBitmapIds[position]);
这样便开启了异步任务,在后台线程内加载我们的图片。
2、对于高效利用已经加载好的图片,我们可以这样理解:因为如果一个Item被destroy后,它就会从它的父容器中移除,然后它的drawable(已经设置好的Bitmap)接着会在某个时刻被gc回收。但是,用户可能会来回滑动页面,那么之前的无用Bitmap其实可以再度利用,而不是重新加载一遍。自然,我们可以想到的是利用LruCache来进行内存缓存,对Bitmap保存一个强引用,这样就不会被gc回收,等到需要用的时候再返回这个Bitmap,对不常用的bitmap进行回收即可。这样便提高了Bitmap的利用效率,不会重复加载Bitmap,也能使内存的消耗保存在一个合理的范围之内。使用LruCache也很简单:
①首先我们在MyPagerAdapyer的构造方法内初始化LruCache:
public MyPagerAdapter(int[] data,Context context){
mBitmapIds = data;
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory * 3 / 8; //缓存区的大小
mCache = new LruCache<Integer, Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight(); //返回Bitmap的大小
}
};
}
②新建一个方法:
public void loadBitmapIntoTarget(Integer id,ImageView imageView){
//首先尝试从内存缓存中获取是否有对应id的Bitmap
Bitmap bitmap = mCache.get(id);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
//如果没有则开启异步任务去加载
new LoadBitmapTask(imageView).execute(id);
}
}
③对LoadBitmapTask作微小的修改,主要是在异步加载任务之后,向内存缓存中添加bitmap:
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
@Override
protected Bitmap doInBackground(Integer... params) {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0]);
//把加载好的Bitmap放进LruCache内
mCache.put(params[0],bitmap);
return bitmap;
}
}
④最后,在我们的instantiate()方法内调用我们的loadBitmapIntoTarget方法即可:
loadBitmapIntoTarget(mBitmapIds[position],imageView);
3、对于最后一种情况,我们可以考虑在加载图片之前,对图片进行缩放,使得图片的规格符合ImageView,那么就不会造成内存的浪费了,那么怎样对一个Bitmap进行缩放呢?
我们知道,一般加载图片都是利用BitmapFactory的几个decode方法来加载,但我们观察这几个方法,会发现它们各自还有一个带options参数的重载方法,即BitmapFactory.Options,那么Bitmap的缩放玄机就在这个Options内。Options有一个成员变量:inSampleSize,采样率,即设置对Bitmap的采样率,比如说inSampleSize默认为1,此时Bitmap的采样宽高等于原始宽高,不做任何改变。如果inSampleSize等于2,那么采样宽高都为原始宽高的1/2,那么大小就变成了原始大小的1/4,因此利用好这个inSampleSize能很好地控制一个Bitmap的大小。具体的使用方法可参考如下:
private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height >= reqHeight || width > reqWidth){
while ((height / (2 * inSampleSize)) >= reqHeight
&& (width / (2 * inSampleSize)) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
//dp转换成px
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
@Override
protected Bitmap doInBackground(Integer... params) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //1、inJustDecodeBounds置为true,此时只加载图片的宽高信息
BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
options.inSampleSize = calculateInSampleSize(options,
dp2px(mContext,240),
dp2px(mContext,360)); //2、根据ImageView的宽高计算所需要的采样率
options.inJustDecodeBounds = false; //3、inJustDecodeBounds置为false,正常加载图片
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
//把加载好的Bitmap放进LruCache内
mCache.put(params[0],bitmap);
return bitmap;
}
}
有一点要说明的是,笔者这里使用的图片是960 * 640的,比ImageView的宽高要小,所以体现不出图片的缩放,读者可以自行改变ImageView的大小,或者加载一张更大规格的图片。
最后,放上修改后MyPagerAdapter.java的完整代码,以供读者参考:
public class MyPagerAdapter extends PagerAdapter {
private int[] mBitmapIds;
private Context mContext;
private LruCache<Integer,Bitmap> mCache;
public MyPagerAdapter(int[] data,Context context){
mBitmapIds = data;
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory * 3 / 8; //缓存区的大小
mCache = new LruCache<Integer, Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
@Override
public int getCount() {
return mBitmapIds.length;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_main,container,false);
ImageView imageView = (ImageView) view.findViewById(R.id.iv);
loadBitmapIntoTarget(mBitmapIds[position],imageView);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
public void loadBitmapIntoTarget(Integer id,ImageView imageView){
//首先尝试从内存缓存中获取是否有对应id的Bitmap
Bitmap bitmap = mCache.get(id);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
//如果没有则开启异步任务去加载
new LoadBitmapTask(imageView).execute(id);
}
}
private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height >= reqHeight || width > reqWidth){
while ((height / (2 * inSampleSize)) >= reqHeight
&& (width / (2 * inSampleSize)) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
private ImageView imageView;
public LoadBitmapTask(ImageView imageView){
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(Integer... params) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //1、inJustDecodeBounds置为true,此时只加载图片的宽高信息
BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
options.inSampleSize = calculateInSampleSize(options,
dp2px(mContext,240),
dp2px(mContext,360)); //2、根据ImageView的宽高计算所需要的采样率
options.inJustDecodeBounds = false; //3、inJustDecodeBounds置为false,正常加载图片
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
//把加载好的Bitmap放进LruCache内
mCache.put(params[0],bitmap);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}
}
最后,感谢你的阅读,希望这篇文章对你有所帮助~
网友评论
public class RotationPageTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE=0.15f;
private static final float rotate = 10;
@Override
public void transformPage(View page, float position) {
Log.e("transformPage", "position"+position+" page" + page.toString());
//position小于等于1的时候,代表page已经位于中心item的最左边,
//此时设置为最小的缩放率以及最大的旋转度数
final float scaleFactor = 1 - MIN_SCALE * Math.abs(position);
final float rotateFactor = rotate * Math.abs(position);
if (position <= -1){
page.setScaleX(Math.max(1-MIN_SCALE, scaleFactor));
page.setScaleY(Math.max(1-MIN_SCALE, scaleFactor));
page.setRotationY(rotate);
}//position从0变化到-1,page逐渐向左滑动
else if (position < 0 && position > -1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(rotateFactor);
}//position从0变化到1,page逐渐向右滑动
else if (position >0 && position < 1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(-rotateFactor);
}//position大于等于1的时候,代表page已经位于中心item的最右边
else if (position >= 1){
page.setScaleX(Math.max(1-MIN_SCALE, scaleFactor));
page.setScaleY(Math.max(1-MIN_SCALE, scaleFactor));
page.setRotationY(-rotate);
} else if (position == 0) {
page.setScaleX(1);
page.setScaleY(1);
page.setRotationY(0);
}
}
}