我們知道,基本上每個 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);
}
}
}
}
好了,就說這些吧,如果再發現什么問題再補充吧。或者大家的火眼金睛發現了問題,也歡迎留言提出來,大家一起學習。