AppIntro 源码学习

更新时间:2016-04-11

功能介绍

AppIntro 是一个引导页,就是我们平时启动应用时一闪而过的广告,当然以下内容和广告没什么关系,实现过程并不复杂,但不得不说作者的配图以及配色使得看上去确实很 cool

总体设计

界面

总的来说可以分成两个部分

  1. 包含多个 Fragments 的 ViewPager ,提供主要的信息展示
  2. 包含提供指示当前页数的 指示器 、跳过按钮以及下一页或完成按钮的底部

Fragments 放在一个 List<Fragment> 中,每次根据 CurrentItem(当前页号)展现相应的 Fragment,相应的指示器中的圆点设为白色点。

流程图

AppIntro_流程图

详细设计

类详细设计

  • AppIntro:实际使用中只要继承这个类,这个类提供了五个抽象方法 init()onSkipPressed()onDonePressed()onSlideChanged()onNextPressed()onCreate 里绑定 Adapter,设置 OnPageChangeListener
final protected void onCreate(Bundle savedInstanceState) {
  //...
  requestWindowFeature(Window.FEATURE_NO_TITLE);
  mPagerAdapter = new PagerAdapter(getSupportFragmentManager(), fragments);
  pager = (AppIntroViewPager) findViewById(R.id.view_pager);
  pager.setAdapter(this.mPagerAdapter);

  pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
      if (slidesNumber > 1) {
        mController.selectPosition(position);
      }
      //  通过滑动上一页可重新启用向右滑动
      if (!pager.isNextPagingEnabled()) {
        if (pager.getCurrentItem() != pager.getLockPage()) {
          setProgressButtonEnabled(baseProgressButtonEnabled);
          pager.setNextPagingEnabled(true);
        } else {
          setProgressButtonEnabled(progressButtonEnabled);
        }
      } else {
        setProgressButtonEnabled(progressButtonEnabled);
      }
      setButtonState(skipButton, skipButtonEnabled);
      onSlideChanged();
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
  });
  //...
}

设置底部按钮的显示/隐藏

public void setProgressButtonEnabled(boolean progressButtonEnabled) {
  this.progressButtonEnabled = progressButtonEnabled;
  if (progressButtonEnabled) {
    if (pager.getCurrentItem() == slidesNumber - 1) {
      setButtonState(nextButton, false);
      setButtonState(doneButton, true);
    } else {
      setButtonState(nextButton, true);
      setButtonState(doneButton, false);
    }
  } else {
    setButtonState(nextButton, false);
    setButtonState(doneButton, false);
  }
}

禁用向右滑动(滑动到下一页)

public void setNextPageSwipeLock(boolean lockEnable) {
  if (lockEnable) {
    // 如果锁定,保存按钮的显示情况,以备恢复锁定时使用
    baseProgressButtonEnabled = progressButtonEnabled;
    setProgressButtonEnabled(!lockEnable);
  } else {
    // 解除锁定后,恢复上一次保存的按钮显示状况
    setProgressButtonEnabled(baseProgressButtonEnabled);
  }
  pager.setNextPagingEnabled(!lockEnable);
}

禁用滑动,而不只是禁用向右滑动

public void setSwipeLock(boolean lockEnable) {
  if (lockEnable) {
    // 如果锁定,保存按钮的显示情况,以备恢复锁定时使用
    baseProgressButtonEnabled = progressButtonEnabled;
  } else {
    // 解除锁定后,恢复上一次保存的按钮显示状况
    setProgressButtonEnabled(baseProgressButtonEnabled);
  }
  pager.setPagingEnabled(!lockEnable);
}

以及还有很多关于自定义底部按钮的方法,这里就省略了。

  • AppIntroViewPager:继承自 ViewPager,通常是配合 Fragment 使用的,便于每个页面的生命周期的控制,对于页面数不多的情况,相应的使用 FragmentPagerAdapter
@Override
public void addOnPageChangeListener(OnPageChangeListener listener) {
  super.addOnPageChangeListener(listener);
  this.listener = listener;
}
@Override
public void setCurrentItem(int item) {
  // 当设定 current item 为 0,即只有一个页面
  // listener 需要我们自己调用
  boolean invokeMeLater = false;
  if (super.getCurrentItem() == 0 && item == 0)
    invokeMeLater = true;
  super.setCurrentItem(item);
  if (invokeMeLater && listener != null)
    listener.onPageSelected(0);
}

检测滑动方向

private boolean detectSwipeToRight(MotionEvent event) {
  // 滑动距离初始为0
  final int SWIPE_THRESHOLD = 0;
  boolean result = false;

  try {
    float diffX = event.getX() - initialXValue;
    if (Math.abs(diffX) > SWIPE_THRESHOLD) {
      if (diffX < 0) {
        result = true;
      }
    }
  } catch (Exception exception) {
    exception.printStackTrace();
  }
  return result;
}

检查页面是否被设定为锁定,以便针对 MotionEvent 做出相应的反应

private boolean checkPagingState(MotionEvent event) {
  if (!pagingEnabled) {
    return true;
  }

  if (!nextPagingEnabled) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
      initialXValue = event.getX();
    }
    if (event.getAction() == MotionEvent.ACTION_MOVE) {
      if (detectSwipeToRight(event)) {
        return true;
      }
    }
  }
  return false;
}
  • DefaultIndicatorController:继承自定义的 IndicatorController ,用于控制当前页号的“展示”之前一直觉得切换的圆点很神奇,看完实现后发现并没有想象中的那么复杂,以下是资源文件
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:right="4.5dp" android:bottom="4.5dp" android:top="4dp">
        <shape android:shape="oval" >
            <!--dot_white_color-->
            <solid android:color="#55222222"/><size android:width="8dp"
            <!--dot_grey_color-->
            <!--<solid android:color="#22222222"/><size android:width="8dp"-->
            android:height="8dp" />         
        </shape>
    </item>
    <item android:right="5dp" android:bottom="5dp" android:top="4dp">
        <shape  android:shape="oval" >
            <!--dot_white-->
            <solid android:color="#ffffff"/><size android:width="8dp"
            <!--dot_grey_color-->
            <!--<solid android:color="#22000000"/><size android:width="8dp"-->
            android:height="8dp" />
        </shape>
    </item>
</layer-list>

Dots 由 ArrayList<ImageView> 管理,遍历数组,将相应的位置的 dot 置为 selected 或 unselected。

@Override
public void selectPosition(int index) {
  mCurrentposition = index;
  for (int i = 0; i < mSlideCount; i++) {
    int drawableId = (i == index) ? (R.drawable.indicator_dot_white) : (R.drawable.indicator_dot_grey);
    Drawable drawable = ContextCompat.getDrawable(mContext, drawableId);
    if (selectedDotColor != DEFAULT_COLOR && i == index)
      drawable.mutate().setColorFilter(selectedDotColor, PorterDuff.Mode.SRC_IN);
    if (unselectedDotColor != DEFAULT_COLOR && i != index)
      drawable.mutate().setColorFilter(unselectedDotColor, PorterDuff.Mode.SRC_IN);
    mDots.get(i).setImageDrawable(drawable);
  }
}

以及初始化 Dots

@Override
public void initialize(int slideCount) {
  mDots = new ArrayList<>();
  mSlideCount = slideCount;
  selectedDotColor = -1;
  unselectedDotColor = -1;

  for (int i = 0; i < slideCount; i++) {
    ImageView dot = new ImageView(mContext);
    dot.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.indicator_dot_grey));

    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.WRAP_CONTENT,
        LinearLayout.LayoutParams.WRAP_CONTENT
    );

    mDotLayout.addView(dot, params);
    mDots.add(dot);
  }

  selectPosition(FIRST_PAGE_NUM);
}
  • PagerAdapter:配合 ViewPager 使用,实际我们只需要实现 getItem()getCount()
  • ViewPageTransformer:提供了多种过渡动画样式:FLOWSLIDE_OVERDEPTHZOOMFADE

一些其他的类

  • PermissionObject:针对 Android 6.0 之后的启动时权限问题。
  • ScrollerCustomDuration:改变 Scroller 的 duration(我猜是过渡动画的时间吧,知道的麻烦告诉我一下

类关系图

AppIntro_类图

总结

看完这个库基本了解了平时看到的顶部 Tab 与页面的切换的实现。关于动画部分不了解所以就 省略了。关于 ViewPager 的更多信息大家可以点下面的参考链接

参考