我们知道,基本上每个 APP 都会有启动引导图,就是启动 APP 时能够左右滑动的大图,滑动到最后一页时,再左滑或是点击“进入”按钮,才进到首页(通常引导图只会显示一次,即显示过就不再显示了)。
同样的,基本每个 APP 首页也都会有幻灯大图,可以左右滑动,或每个几秒自动滚动。而引导图跟幻灯实现起来其实很类似,闲着没事,使用 ViewPager 实现了一下此功能。工程源码在这里:https://github.com/JulyDev/AppGuide
最终效果:
Talk is cheap, show you the code.
工程结构
其中 FirstActivity
是启动 Activity, MainActivity
模拟的是首页, WelcomeGuideActivity
就是引导页啦。启动 APP 时,首先会打开 FirstActivity
,然后是进到首页,在首页先判断引导图是不是显示过,若没显示过则先展示引导图(引导图一般只显示一次,若清除数据或重新安装APP则会重新显示引导图),引导图展示完毕回到首页,逻辑就是这么简单。
FirstAcitivity
代码很简单:
public class FirstActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
// 根据需要,做些初始化操作
// init();
// 模拟跳转MainActivity时机
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
startActivity(new Intent(FirstActivity.this, MainActivity.class));
finish();
}
}, 1000);
}
}
显示引导页的逻辑放在了MainActivity
里:
/**
* 首页
*/
public class MainActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 如果没有显示过引导图,则显示之(为了方便查看效果,此处把判断条件注释掉了)
// if (ConfigUtil.needShowGuide(this))
{
startActivity(new Intent(this, WelcomeGuideActivity.class));
}
// 首页其他部分该怎么显示就怎么显示
// ……
}
}
下面重点看一下引导图页面的实现逻辑。
引导图实现逻辑
启动引导图一般要求可以左右滑动(用 ViewPager 就能实现啦),右上角有“跳过”字样,点击就直接进到首页,不再展示剩下的引导图了。最后一页引导图一般会有一个进入 APP 的按钮,点击即可关闭引导图,进入到首页。
另外,引导图下方一般都会有圆点点,表示引导图个数,并突出显示当前所在图片的位置。这些点点的实现方式有两种,一是切图时让设计直接切在图片上,二是自己手动去实现。我通过自定义 View 来实现的(PonitView)。
在此基础上,我又增加了两个功能:
- 滑动到最后一页时,继续滑动,也能进入首页,且是平滑过渡,不会显得那么突兀;
- 做了View的缓存,可以减少内存的占用。
其实就引导图而言,这个缓存可有可无,因为引导图个数一般不会太多张,而缓存对于超过三张的图片才会有效果。不过为了记录知识点,我还是加了缓存策略,这样以后做首页幻灯那种效果也是可以拿来直接使用的,哇哈哈。
public class WelcomeGuideActivity extends Activity
{
private static final String TAG = "WelcomeGuideActivity";
/**
* 引导图个数
*/
private static final int COUNTS = 4;
/**
* View 最大缓存个数
*/
private static final int MAX_CACHE_COUNT = 3;
private ViewPager viewPager;
/**
* View缓存,考虑view的复用,只需要三个view就够了
*/
private ArrayList<View> viewList = new ArrayList<View>(MAX_CACHE_COUNT);
private GuideAdapter adapter;
/**
* 当前在第几个图片
*/
private int currentPosition;
/**
* 引导图下方的点点,会突出显示当前滑动到第几个
*/
private PointView pointView;
// 本地图片id
private int[] resIds = {R.mipmap.guide1, R.mipmap.guide2, R.mipmap.guide3,R.mipmap.guide4};
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome_guide);
initViews();
}
private void initViews()
{
viewList.clear();
for (int i = 0; i < MAX_CACHE_COUNT; i++)
{
View pageView = View.inflate(this, R.layout.welcome_guide_view, null);
ViewHolder holder = new ViewHolder();
holder.image = (ImageView) pageView.findViewById(R.id.guide_image);
holder.skip = (TextView) pageView.findViewById(R.id.skip);
holder.entry = (ImageView) pageView.findViewById(R.id.use_at_once);
pageView.setTag(holder);
viewList.add(pageView);
}
viewPager = (ViewPager) findViewById(R.id.guide_viewpager);
adapter = new GuideAdapter();
viewPager.setAdapter(adapter);
// 为 1 的时候可以不用手动设置了,默认就是 1
// viewPager.setOffscreenPageLimit(1);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener()
{
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
{
}
@Override
public void onPageSelected(int position)
{
currentPosition = position;
pointView.setSelectedPosition(position);
Log.d(TAG, " onPageSelected position = " + position);
}
@Override
public void onPageScrollStateChanged(int state)
{
}
});
viewPager.setOnTouchListener(new View.OnTouchListener()
{
float startX, endX;
@Override
public boolean onTouch(View v, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
startX = event.getX();
break;
case MotionEvent.ACTION_UP:
try
{
endX = event.getX();
// 首先要确定的是,是否到了最后一页,然后判断是否向左滑动,并且滑动距离是否大于某段距离,这里的判断距离是屏幕宽度的四分之一(可以适当控制)
if (currentPosition == (COUNTS - 1)
&& (startX - endX) >= (screenWidthPx(WelcomeGuideActivity.this) / 4))
{
enterMainActivity();
}
}
catch (Exception e)
{
Log.e("Exception", e + "");
}
break;
}
return false;
}
});
// 添加点点
pointView = (PointView) findViewById(R.id.point_view);
pointView.addPoints(COUNTS);
pointView.setSelectedPosition(0);
}
class GuideAdapter extends PagerAdapter
{
@Override
public Object instantiateItem(ViewGroup container, int position)
{
View view = createItemView(position);
container.removeView(view);
container.addView(view);
Log.d(TAG, " instantiateItem position = " + position + ",view pos = " + position % MAX_CACHE_COUNT + ",container size = " + container.getChildCount());
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object)
{
// 不在此处删除(在此处删除,显示可能会有问题),在instantiateItem里addView前删除
// container.removeView(viewList.get(position % MAX_CACHE_COUNT));
Log.d(TAG, " destroyItem position = " + position);
}
@Override
public int getCount()
{
return COUNTS;
}
@Override
public boolean isViewFromObject(View view, Object object)
{
return view == object;
}
}
/**
* ViewPager 每一页View
*
* @param position
* @return
*/
private View createItemView(int position)
{
if (position >= COUNTS || position < 0)
{
return null;
}
// 注意这里要取缓存列表里的View,所以position范围只能是0,1,2,取模即可
int pos = position % MAX_CACHE_COUNT;
View view = viewList.get(pos);
ViewHolder holder = (ViewHolder) view.getTag();
holder.image.setImageResource(resIds[position]);
View useAtOnce = holder.entry;
View skip = holder.skip;
skip.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
enterMainActivity();
}
});
if (position < COUNTS - 1)
{
// 只显示右上角"跳过"
useAtOnce.setVisibility(View.GONE);
skip.setVisibility(View.VISIBLE);
}
else if (position == COUNTS - 1)
{
// 最后一页
useAtOnce.setVisibility(View.VISIBLE);
skip.setVisibility(View.GONE);
}
useAtOnce.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
enterMainActivity();
}
});
return view;
}
/**
* 关闭引导界面,进入首页
*/
private void enterMainActivity()
{
finish();
}
/**
* 小的为屏幕宽度
*
* @param context
* @return
*/
public static int screenWidthPx(Context context)
{
int widthPx = context.getResources().getDisplayMetrics().widthPixels;
int heightPx = context.getResources().getDisplayMetrics().heightPixels;
return widthPx > heightPx ? heightPx : widthPx;
}
private static class ViewHolder
{
/**
* 引导图
*/
public ImageView image;
/**
* 跳过
*/
public TextView skip;
/**
* 立即使用按钮
*/
public ImageView entry;
}
}
下面说一下实现过程中,需要注意的地方:
- 滑动到最后一页时,继续滑动,也能进入首页,且是平滑过渡,不会显得那么突兀;
首先,重写ViewPager的setOnTouchListener,代码往上翻……
然后,给Activity加切换动画,我是通过设置 Activity 的主题的方式来实现的,加一个右进左出的动画就可以了。在AndroidManifest.xml
里设置如下:
<!-- 首页 -->
<activity
android:name="com.july.welcomeguide.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/RightInLeftOutTheme"
android:windowSoftInputMode="adjustPan">
</activity>
<!--App启动引导界面-->
<activity
android:name="com.july.welcomeguide.WelcomeGuideActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/RightInLeftOutTheme"
android:windowSoftInputMode="adjustPan" >
</activity>
其中 RightInLeftOutTheme
是这样子的:
<style name="RightInLeftOutTheme" parent="@android:style/Theme.NoTitleBar">
<item name="android:windowAnimationStyle">@style/RightInLeftOutAnimation</item>
</style>
<!-- 右进左出动画-->
<style name="RightInLeftOutAnimation" parent="@android:style/Animation">
<item name="android:activityOpenEnterAnimation">@anim/slide_right_in</item>
<item name="android:activityOpenExitAnimation">@anim/slide_left_out</item>
<item name="android:activityCloseEnterAnimation">@anim/slide_right_in</item>
<item name="android:activityCloseExitAnimation">@anim/slide_left_out</item>
</style>
- 关于 View 缓存遇到的坑
我们知道,ViewPager 有个setOffscreenPageLimit(int limit) 方法,源码定义如下:
/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
* <p>This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.</p>
*
* @param limit How many pages will be kept offscreen in an idle state.
*/
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
}
意思大概就是说我们可以设置在空闲状态的视图层次结构中,应该保留在当前页的任意一侧的页面数,不手动设置的话,默认的就是1,也就是保留当前页(左)右两侧各一个。调整这个值,能够优化页面切换的流畅度,如果页面个数比较少的话(3-4)也可以不用缓存,把页面全部创建出来并保持激活状态,这样前后切换创建新布局的耗时更少。
如上述所言,针对引导图比较少的情况,View 可以不用缓存,即有多少页面就创建多少个View,这个很简单。加了缓存逻辑也没什么坏处,也方便以后的扩展。
- 坑一
View缓存的个数最大就是3个,这个一定要跟引导图的总个数别搞混了,如果COUNTS == MAX_CACHE_COUNT ,就相当于没做缓存。
/**
* 引导图个数
*/
private static final int COUNTS = 4;
private static final int MAX_CACHE_COUNT = 3;
private ViewPager viewPager;
/**
* View缓存,考虑view的复用,只需要三个view就够了
*/
private ArrayList<View> viewList = new ArrayList<View>(MAX_CACHE_COUNT);
//此处省略n行代码
……
private void initViews()
{
viewList.clear();
for (int i = 0; i < MAX_CACHE_COUNT; i++)
{
View pageView = View.inflate(this, R.layout.welcome_guide_view, null);
ViewHolder holder = new ViewHolder();
holder.image = (ImageView) pageView.findViewById(R.id.guide_image);
holder.skip = (TextView) pageView.findViewById(R.id.skip);
holder.entry = (ImageView) pageView.findViewById(R.id.use_at_once);
pageView.setTag(holder);
viewList.add(pageView);
}
//此处省略n行代码
……
}
-坑二
因为使用了缓存View,所以不能在destroyItem里去移除老的 View,在引导图超过3个时,移除时会导致页面闪动,而且显示错乱。解决方法就是在instantiateItem()方法里在 container.addView(view);之前,调用 container.removeView(view);就可以了。
class GuideAdapter extends PagerAdapter
{
@Override
public Object instantiateItem(ViewGroup container, int position)
{
View view = createItemView(position);
container.removeView(view);
container.addView(view);
Log.d(TAG, " instantiateItem position = " + position + ",view pos = " + position % MAX_CACHE_COUNT + ",container size = " + container.getChildCount());
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object)
{
// 不在此处删除(在此处删除,显示可能会有问题),在instantiateItem里addView前删除
// container.removeView(viewList.get(position % MAX_CACHE_COUNT));
Log.d(TAG, " destroyItem position = " + position);
}
……
}
3.自定义点点
直接上代码吧:
public class PointView extends LinearLayout {
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PointView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER);
}
/**
* 设置当前选中的点点位置
* @param position
*/
public void setSelectedPosition(int position)
{
int count = getChildCount();
for (int i = 0; i < count; i++)
{
getChildAt(i).setEnabled(i == position);
}
}
/**
* 添加点点(外部调用接口)
* @param size
*/
public void addPoints(int size)
{
addPointBtn(size, R.drawable.point_btn_bg, 8, 8, 16);
}
/**
* 添加点点
* @param size 点点个数
* @param imageId
* @param width 单位dp
* @param height 单位dp
* @param margin 单位dp
*/
private void addPointBtn(int size, int imageId, int width, int height, int margin)
{
removeAllViews();
if (size <= 0)
{
return;
}
ImageView imageView;
for (int i = 0; i < size; i++)
{
imageView = new ImageView(getContext());
imageView.setBackgroundResource(imageId);
imageView.setEnabled(false);
addView(imageView, ConvertUtil.dip2px(getContext(), width), ConvertUtil.dip2px(getContext(), height));
LinearLayout.LayoutParams params = (LayoutParams) imageView.getLayoutParams();
if(i == size - 1)
{
params.setMargins(0, 0, 0, 0);
}
else
{
params.setMargins(0, 0, ConvertUtil.dip2px(getContext(), margin), 0);
}
}
}
}
好了,就说这些吧,如果再发现什么问题再补充吧。或者大家的火眼金睛发现了问题,也欢迎留言提出来,大家一起学习。