APP啟動引導圖

我們知道,基本上每個 APP 都會有啟動引導圖,就是啟動 APP 時能夠左右滑動的大圖,滑動到最后一頁時,再左滑或是點擊“進入”按鈕,才進到首頁(通常引導圖只會顯示一次,即顯示過就不再顯示了)。
同樣的,基本每個 APP 首頁也都會有幻燈大圖,可以左右滑動,或每個幾秒自動滾動。而引導圖跟幻燈實現起來其實很類似,閑著沒事,使用 ViewPager 實現了一下此功能。工程源碼在這里:https://github.com/JulyDev/AppGuide

最終效果:

app_guide.gif

Talk is cheap, show you the code.

工程結構

image.png

其中 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)。
在此基礎上,我又增加了兩個功能:

  1. 滑動到最后一頁時,繼續滑動,也能進入首頁,且是平滑過渡,不會顯得那么突兀;
  2. 做了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;
    }
}

下面說一下實現過程中,需要注意的地方:

  1. 滑動到最后一頁時,繼續滑動,也能進入首頁,且是平滑過渡,不會顯得那么突兀;
    首先,重寫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>
  1. 關于 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);
            }
        }
    }
}

好了,就說這些吧,如果再發現什么問題再補充吧。或者大家的火眼金睛發現了問題,也歡迎留言提出來,大家一起學習。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,065評論 25 708
  • W:復習藥理兩章,完成經絡背誦 O:完成學習任務順利完成考試,自己做其他事情時會更加安心 O:時間分配問題,支教隊...
    高N少女閱讀 116評論 0 0
  • 現如今,有很多的學生上了大學突然不知道做什么了,曾經的高三忙著理所應當、天經地義,只為了高考后能上個好大學;而上了...
    遇見阿文閱讀 2,551評論 1 5
  • 周末朋友相約帶著孩子去鄉下摘核桃。 我們開車,很快來到了成都附近的龍泉山腳下。 朋友的父母在這里修了一棟兩層樓的農...
    香香草閱讀 470評論 0 3