APP启动引导图

作者: Vivi成长吧 | 来源:发表于2017-08-03 12:34 被阅读435次

    我们知道,基本上每个 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);
                }
            }
        }
    }
    

    好了,就说这些吧,如果再发现什么问题再补充吧。或者大家的火眼金睛发现了问题,也欢迎留言提出来,大家一起学习。

    相关文章

      网友评论

        本文标题:APP启动引导图

        本文链接:https://www.haomeiwen.com/subject/pcxylxtx.html