Gallery 的三种实现方式

主要内容:

目标效果:

需求分析

从目标效果上来看主要考虑以下几个点:

  1. 图片如何切换和切换动画
    • ImageSwitcher 提供了 setImageResource (int resid) 方法对下一个 ImageView 进行图片加载和切换,切换的动画效果需要自定义
    • ViewPager 的切换和切换动画内部处理
    • HorizontalScrollView 可以通过 smoothScrollBy (int dx, int dy) 方法进行平滑切换
  2. 图片的切换方向
    • ImageSwitcher 的切换方向需要自己判断
    • HorizontalScrollView 的切换方向虽然不用判断,但是滑动距离需要自己控制
    • ViewPager 的切换方向无需处理
  3. 确定切换时应该加载的图片
    • ImageSwitcher 和 HorizontalScrollView 加载图片需要自己判断
    • ViewPager 的加载图片无需处理
  4. 图片下方的圆点效果,与图片数量一致
  5. 圆点和图片的同步切换

ImageSwitcher 实现

  1. ImageSwitcher 的特点 首先查看 ImageSwitcher 和 ViewSwitcher 的官方 API:

    ViewSwitcher that switches between two ImageViews when a new image is set on it. The views added to an ImageSwitcher must all be ImageViews.

    ViewAnimator that switches between two views, and has a factory from which these views are created. You can either use the factory to create the views, or add them yourself. A ViewSwitcher can only have two child views, of which only one is shown at a time.

    官方给出的 API 说明了,ImageSwitcher 里最多只能由两个 ImageView,同一时间只能显示一个,在两个 ImageView 之间能够设置视图切换动画,并且提供了 factory 用于创建两个 ImageView(当然也可以选择自行添加)。

  2. 实现过程 首先我们需要在布局文件中添加 ImageSwitcher 控件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.rookieyang.gallerytest.ImageSwitcherTest">

    <ImageSwitcher
    android:id="@+id/imageSwitcher"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="30dp">
    <LinearLayout
    android:id="@+id/tipsLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:gravity="center_horizontal"
    android:orientation="horizontal"/>
    </RelativeLayout>
    </FrameLayout>
    > 采用 FrameLayout 将圆点部分置于图片的上方,RelativeLayout 用于控制原点部分在底部,LinearLayout 中的属性设置用于将圆点水平居中放置。

    在添加完 ImageSwiter 之后,通过 setFactory(ViewFactory factory) 函数为 ImageSwiter 添加 ImageView:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mImageSwitcher.setFactory(new ViewFactory() {
    @Override
    public View makeView() {
    ImageView imageView = new ImageView(ImageSwitcherTest.this);
    ImageSwitcher.LayoutParams layoutParams = new ImageSwitcher.LayoutParams(
    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    imageView.setScaleType(ScaleType.CENTER_CROP);
    imageView.setLayoutParams(layoutParams);
    return imageView;
    }
    });
    > 这里为 ImageSwitcher 设置了一个 factory,在 setFactory(ViewFactory factory) 内部会执行两次 obtainView() 完成 View 的增加。

    上述过程只是完成了 ImageView 的添加,实际的图片还没有被加载,由于 ImageSwitcher 的限制,所以我们不能直接将 ImageView 一次性添加进去(这样性能也不高),可以选择用一个数组对图片资源进行保存,在调用 setImageResource() 时进行加载:

    1
    2
    private int[] mImageId = new int[]{R.drawable.pic1, R.drawable.pic2, R.drawable.pic3};
    mImageSwitcher.setImageResource(mImageId[0]);
    在解决了如何加载图片之后,还需要为图片下方添加圆点效果,圆点的个数由图片的个数决定,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private ImageView[] mTips;

    mTips = new ImageView[mImageId.length];
    LinearLayout tipsLinearLayout = (LinearLayout) findViewById(R.id.tipsLayout);
    for (int i = 0; i < mImageId.length; i++) {
    mTips[i] = new ImageView(this);
    LinearLayout.LayoutParams layoutParams = new LayoutParams(
    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    ViewGroup.LayoutParams.WRAP_CONTENT));
    layoutParams.leftMargin = 5;
    layoutParams.rightMargin = 5;
    tipsLinearLayout.addView(mTips[i], layoutParams);
    }

    setTipsImage(0);

    private void setTipsImage(int position) {
    for (int i = 0; i < mImageId.length; i++) {
    if (i == position) {
    mTips[i].setImageResource(R.drawable.page_indicator_focused);
    } else {
    mTips[i].setImageResource(R.drawable.page_indicator_unfocused);
    }
    }
    }
    在完成上述步骤之后我们还需要完成的是让图片和圆点同时进行切换、确定切换方向和应该加载的图片以及切换的动画,对于图片的切换方向可以依据第一次按住屏幕和离开屏幕这两点的位置进行判断,而切换时加载的图片则可以用一个变量对当前图片序号进行保存,动画效果则可以定义四个动画(左进,左出,右进、右出)文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <!--left_in-->
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
    android:fromXDelta="-100%p"
    android:toXDelta="0"
    android:duration="500"/>
    </set>
    <!--left_out-->
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
    android:fromXDelta="0"
    android:toXDelta="-100%p"
    android:duration="500"/>
    </set>
    <!--right_in-->
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
    android:fromXDelta="100%p"
    android:toXDelta="0"
    android:duration="500"/>
    </set>
    <!--right_out-->
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
    android:fromXDelta="0"
    android:toXDelta="100%p"
    android:duration="500"/>
    </set>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
        private int mDownX;
    private int mCurrentPosition = 0;

    mImageSwitcher.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    mDownX = (int) event.getX();
    break;
    case MotionEvent.ACTION_UP:
    int upX = (int) event.getX();
    if (upX > mDownX) {
    if (mCurrentPosition > 0) {
    mImageSwitcher.setInAnimation(ImageSwitcherTest.this,
    R.anim.left_in);
    mImageSwitcher.setOutAnimation(ImageSwitcherTest.this,
    R.anim.right_out);
    mCurrentPosition--;
    mImageSwitcher.setImageResource(mImageId[mCurrentPosition]);
    setTipsImage(mCurrentPosition);
    }
    } else if (upX < mDownX) {
    if (mCurrentPosition < mImageId.length - 1) {
    mImageSwitcher.setInAnimation(ImageSwitcherTest.this,
    R.anim.right_in);
    mImageSwitcher.setOutAnimation(ImageSwitcherTest.this,
    R.anim.left_out);
    mCurrentPosition++;
    mImageSwitcher.setImageResource(mImageId[mCurrentPosition]);
    setTipsImage(mCurrentPosition);
    }
    }
    break;
    }
    return true;
    }
    });
    ​```

    ## ViewPager 实现

    1. ViewPager 的特点

    > ViewPager is most often used in conjunction with Fragment, which is a convenient way to supply and manage the lifecycle of each page. There are standard adapters implemented for using fragments with the ViewPager, which cover the most common use cases. These are FragmentPagerAdapter and FragmentStatePagerAdapter; each of these classes have simple code showing how to build a full user interface with them.

    ViewPager 一般结合 Fragment 使用,每个页面就是一个 Fragment,系统提供了 FragmentPagerAdapter 和 FragmentStatePagerAdapter 用于填充 ViewPager。

    2. 实现过程
    首先向布局文件中添加 ViewPager 控件:

    ```XML
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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.rookieyang.gallerytest.ViewPagerTest">

    <android.support.v4.view.ViewPager
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="30dp">
    <LinearLayout
    android:id="@+id/tipsFragmentLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:gravity="center_horizontal"
    android:orientation="horizontal"/>
    </RelativeLayout>

    </FrameLayout>
    布局基本与 ImageSwitcher 一致,只是将 ImageSwitcher 控件替换为 ViewPager。 接下来需要将图片加载到 ViewPager 中去,可以利用 FragmentPagerAdapter 达到这一目的,由于 FragmentPagerAdapter 返回的是 Fragment,所以创建了 ImageFragment 类,用于将 Image 放置到 Fragment中,代码布局如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class ImageFragment extends Fragment {

    private View view;
    private int imageViewRes;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    view = inflater.inflate(R.layout.fragment_image, container, false);
    return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ImageView imageView = (ImageView) view.findViewById(R.id.viewPageImage);
    imageView.setScaleType(ScaleType.CENTER_CROP);
    imageView.setImageResource(imageViewRes);
    }

    public void setImageView(int imageViewRes) {
    this.imageViewRes = imageViewRes;
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <FrameLayout 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.rookieyang.gallerytest.ImageFragment">
    <ImageView
    android:id="@+id/viewPageImage"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
    </FrameLayout>

    接下来实现继承自 FragmentPagerAdapter 的 ImagePagerAdapter 类,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ImagePagerAdapter extends FragmentPagerAdapter {

    private List<ImageFragment> mFragments;

    public ImagePagerAdapter(FragmentManager fm, List<ImageFragment> fragments) {
    super(fm);
    mFragments = fragments;
    }

    @Override
    public ImageFragment getItem(int position) {
    Log.i("getItem", "getItem: " + mFragments.get(position).getId());
    return mFragments.get(position);
    }

    @Override
    public int getCount() {
    return mFragments.size();
    }
    }
    在完成上述的步骤之后,利用保存图片资源的数组创建对应个数的 ImageFragment 并保存到 List 中,然后创建一个 ImagePagerAdapter 对象,替 ViewPager 设置适配器即可实现图片的加载和滑动。由于 ViewPager 内部处理了滑动方向和下一张图片加载判断的问题,所以只剩下图片与圆点的同步切换需要进行处理,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    mImageIds = new int[]{R.drawable.pic1, R.drawable.pic2, R.drawable.pic3};
    mTips = new ImageView[mImageIds.length];
    mViewPager = (ViewPager) findViewById(R.id.viewPager);
    ViewGroup viewGroup = (ViewGroup) findViewById(R.id.tipsFragmentLayout);
    LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
    new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    layoutParams.rightMargin = 5;
    layoutParams.leftMargin = 5;
    for (int i = 0; i < mImageIds.length; i++) {
    ImageFragment imageFragment = new ImageFragment();
    imageFragment.setImageView(mImageIds[i]);
    mImageFragments.add(imageFragment);
    if (mImageIds.length > 1) {
    mTips[i] = new ImageView(this);
    mTips[i].setImageResource(R.drawable.page_indicator_unfocused);
    viewGroup.addView(mTips[i], layoutParams);
    mTips[mCurrentPosition].setImageResource(R.drawable.page_indicator_focused);
    }
    }

    ImagePagerAdapter imagePagerAdapter = new ImagePagerAdapter(
    getSupportFragmentManager(), mImageFragments);
    mViewPager.setAdapter(imagePagerAdapter);
    mViewPager.addOnPageChangeListener(new OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset,
    int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
    if (mImageIds.length > 1) {
    for (int i = 0; i < mImageIds.length; i++) {
    if (i == position) {
    mTips[i].setImageResource(R.drawable.page_indicator_focused);
    } else {
    mTips[i].setImageResource(R.drawable.page_indicator_unfocused);
    }
    }
    }
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
    });

HorizontalScrollView 实现

  1. HorizontalScrollView 特点

    A HorizontalScrollView is a FrameLayout, meaning you should place one child in it containing the entire contents to scroll; this child may itself be a layout manager with a complex hierarchy of objects. A child that is often used is a LinearLayout in a horizontal orientation, presenting a horizontal array of top-level items that the user can scroll through.

    从这里可以了解到如果要实现上面的效果,那么应该在 HorizontalScrollView 放置一个 LinearLayout,然后 LinearLayout 内加载的是要显示的图片,由于是水平滑动,所以 LinearLayout 的方向也要是水平的。

  2. 实现过程 依旧是添加了 HorizontalScrollView 控件,把放置 ImageSwwitcher 的部分替换为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <HorizontalScrollView
    android:id="@+id/horizontalScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbars="none">
    <LinearLayout
    android:id="@+id/imageLayout"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="horizontal" />
    </HorizontalScrollView>
    > 为了效果一致去掉了滚动条。

    添加完控件之后,就需要填充 HorizontalScrollView ,为了扩展性,所以创建了一个 HorizontalViewAdapter 类,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class HorizontalViewAdapter{

    private int[] mImageResIdList;
    private Context mContext;

    public HorizontalViewAdapter(Context context, int[] imageResIdList) {
    mContext = context;
    mImageResIdList = imageResIdList;
    }
    public int getCount() {
    return mImageResIdList.length;
    }

    public ImageView getItem(int position) {
    ImageView imageView = new ImageView(mContext);

    WindowManager wm = (WindowManager) mContext.
    getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(displayMetrics);
    int width = displayMetrics.widthPixels;
    int height = displayMetrics.heightPixels;
    Log.i("Adpter: ", String.valueOf(width));

    LinearLayout.LayoutParams layoutParams = new LayoutParams(
    new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
    layoutParams.width = width;
    layoutParams.height = height;

    imageView.setLayoutParams(layoutParams);
    imageView.setScaleType(ScaleType.CENTER_CROP);
    imageView.setImageResource(mImageResIdList[position]);
    return imageView;
    }
    }

    这里图片的长和宽会根据设备的分辨率进行指定,之所以不用 match_parent 的原因是这样会导致显示效果出问题。

    在创建完适配器之后,由于 HorizontalScrollView 没有设置适配器的方法,所以需要实现 setAdapter() 方法,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public void setAdapter(HorizontalViewAdapter horizontalViewAdapter) {
    mTips = new ImageView[mImageId.length];
    LinearLayout imageLayout = (LinearLayout) findViewById(R.id.imageLayout);
    LinearLayout tipsLinearLayout = (LinearLayout) findViewById(R.id.horizontalTipsLayout);
    int size = horizontalViewAdapter.getCount();
    for (int i = 0; i < size; i++) {
    ImageView imageView = horizontalViewAdapter.getItem(i);
    imageLayout.addView(imageView);

    mTips[i] = new ImageView(this);
    LinearLayout.LayoutParams layoutParams = new LayoutParams(
    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    ViewGroup.LayoutParams.WRAP_CONTENT));
    layoutParams.leftMargin = 5;
    layoutParams.rightMargin = 5;
    tipsLinearLayout.addView(mTips[i], layoutParams);
    }
    }

    实现 setAdapter() 方法之后,就可以在 onCreate() 方法中调用了,调用之后图片会被加载到布局中,这时候也可以滑动,但是由于滑动距离没有控制,所以并没有达到想要的效果,由于图片的长度取决于设备的分辨率,所以可以获取设备的长度作为滑动距离,为 HorizontalScrollView 设置监听事件,当触摸屏幕之后进行判断,确定实际滑动的方向,具体代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //获取图片滑动距离
    WindowManager wm = (WindowManager) getApplicationContext().
    getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(displayMetrics);
    mScrollX = displayMetrics.widthPixels;
    //确定的滑动方向
    final HorizontalScrollView horizontalScrollView = (HorizontalScrollView) findViewById(
    R.id.horizontalScrollView);
    horizontalScrollView.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    mDownX = (int) event.getX();
    break;
    case MotionEvent.ACTION_UP:
    int upX = (int) event.getX();
    if (upX > mDownX) {
    if (mCurrentPosition > 0) {
    mCurrentPosition--;
    horizontalScrollView.smoothScrollBy(-mScrollX, 0);
    setTipsImage(mCurrentPosition);
    }
    } else if (upX < mDownX) {
    if (mCurrentPosition < mImageId.length - 1) {
    mCurrentPosition++;
    Log.i("onTouch: ", "onTouch: " + mCurrentPosition);
    horizontalScrollView.smoothScrollBy(mScrollX, 0);
    setTipsImage(mCurrentPosition);
    }
    }
    break;
    }
    return true;
    }
    });

总结

HorizontalScrollView 的实现实际上并不完善,因为 ImageView 是一次性全部添加进去的,并没有做优化处理,在虚拟机上运行时,由于超过了堆内存会导致报错。