Android引导页的魅力
几乎所有的APP都有引导页,可见引导页的魅力有多大,引导页能迅速抓住用户的眼球,让用户很快的了解该app的主张。一个好的引导页能提升用户体验,增强用户好感度,甚至有些应用优美的引导页让人有卸载重装的冲动,就为了再看一遍引导页。那么,app的引导页都用什么做的呢?大致可以分为3类:
1、普通的viewpager引导页
这是最普遍的一种做法,很多app就是使用这种方式实现,刚开始流行的时候可能会觉得比较新奇,但是,时间久了,人们都形成了审美疲劳了。
2、视差引导页
这种比第一种方式强很多,但是用得不多,为什么呢?因为第三种。这种方式开起来很炫酷,很牛B,但是越炫酷越难实现(相比于html5来说)。
3、HTML5引导页
现在越来越多的app开始使用这种方式,HTML5在动画方面很强大,所以能够弄出来很炫酷,很牛逼的效果。
普通的Viewpager引导页
先上效果图:
请忽略图片中的指示器,那是之前的项目设计为了省事直接将指示器放在了图片上面,请不要过分关注。
1. 在xml布局文件中添加
<?xml version="1.0"encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/normal_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v4.view.ViewPager>
<Button
android:layout_width="@dimen/act_wel_btn_width"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="@dimen/act_wel_btn_textSize"
android:layout_centerHorizontal="true"
android:id="@+id/act_welcom_btn_start"
android:layout_marginBottom="@dimen/act_wel_btn_marginBottom"
android:layout_alignParentBottom="true"
android:background="@null"
android:visibility="gone"
/>
</RelativeLayout>
2. 编写Adapter类
public class NormalPagerAdapter extends PagerAdapter { private Context context; private List<ImageView> mDatas; public NormalPagerAdapter(Context context, List<ImageView> mDatas) { this.context = context; this.mDatas = mDatas; } @Override public int getCount() { return mDatas.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view.equals(object); } @Override public void destroyItem(ViewGroup container, int position, Object object) { ((ViewPager) container).removeView((ImageView) object); } @Override public ImageView instantiateItem(ViewGroup container, int position) { container.addView(mDatas.get(position)); return mDatas.get(position); } }
3. 初始化Viewpager,初始化视图数据、添加监听器,等等
public class NormalViewPager extends Activity { private ViewPager viewPager; private List<ImageView> mDatas; private NormalPagerAdapter adapter; private Button startBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_normal_view_pager); viewPager = (ViewPager) findViewById(R.id.normal_view_pager); startBtn = (Button) findViewById(R.id.act_welcom_btn_start); startBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { NormalViewPager.this.finish(); } }); initData(); initAdapter(); } private void initData() { ImageView view1 = new ImageView(this, null); view1.setBackgroundResource(R.drawable.pic_wel_a); ImageView view2 = new ImageView(this); view2.setBackgroundResource(R.drawable.pic_wel_b); ImageView view3 = new ImageView(this); view3.setBackgroundResource(R.drawable.pic_wel_c); ImageView view4 = new ImageView(this); view4.setBackgroundResource(R.drawable.pic_wel_d); ImageView view5 = new ImageView(this); view5.setBackgroundResource(R.drawable.pic_wel_e); mDatas = new ArrayList<>(); mDatas.add(view1); mDatas.add(view2); mDatas.add(view3); mDatas.add(view4); mDatas.add(view5); } private void initAdapter() { adapter = new NormalPagerAdapter(this, mDatas); viewPager.setAdapter(adapter); viewPager.setPageTransformer(true, new DepthPageTransformer()); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { switch (position) { case 0: startBtn.setVisibility(View.GONE); break; case 1: startBtn.setVisibility(View.GONE); break; case 2: startBtn.setVisibility(View.GONE); break; case 3: startBtn.setVisibility(View.GONE); break; case 4: startBtn.setVisibility(View.VISIBLE); break; } } @Override public void onPageScrollStateChanged(int state) { } }); } }
上面的代码添加了监听器,同时设置了按钮的显示事件,viewpager是最简单也是目前比较普遍的引导页实现方式,就不做过多的讲解。
视差引导页
先上两张两张效果图,一张是ecmobile稍微改了一下,另一张是小红书
效果是不是很不错!那么简单说一下实现原理,原理就一句话:层级不同,滑动速度不同。
Ecmobile的实现
1. xml文件
由于xml代码有点儿多,就不贴出来了,这里只是给出布局层级图
层级为3层
· 背景图层 back_image_one
· layer层 内含FrameLayout,一个FrameLayout对应一个ViewPager的pager页面。我们要控制的就是FrameLayout中的元素的滑动速度
· ViewPager层
2. Adapter的实现
代码较多,我只贴instantiateItem函数
@Override public Object instantiateItem(ViewGroup container, int position) { final ViewHolde holder; holder = new ViewHolde(); View imageLayout = mInflater.inflate(R.layout.gallery_image_item, null); holder.image = (LinearLayout) imageLayout.findViewById(R.id.gallery_image_item_view); if (position == 4) { holder.image.setEnabled(true); } else { holder.image.setEnabled(false); } if (position == 0) { holder.image.removeAllViews(); View view0 = inflater.inflate(R.layout.lead_a, null); holder.image.addView(view0); } else if (position == 1) { holder.image.removeAllViews(); View view1 = inflater.inflate(R.layout.lead_b, null); holder.image.addView(view1); } else if (position == 2) { holder.image.removeAllViews(); View view2 = inflater.inflate(R.layout.lead_c, null); holder.image.addView(view2); } else if (position == 3) { holder.image.removeAllViews(); View view3 = inflater.inflate(R.layout.lead_d, null); holder.image.addView(view3); } else if (position == 4) { holder.image.removeAllViews(); View view4 = inflater.inflate(R.layout.lead_e, null); holder.image.addView(view4); } ((ViewPager) container).addView(imageLayout, 0); return imageLayout; }
看看gallery_image_item.xml文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#00000000" android:orientation="vertical" > <FrameLayout android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:id="@+id/gallery_image_item_view" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > </LinearLayout> </FrameLayout> </LinearLayout>
结合代码我们知道,逻辑就是根据position,向这个布局中添加对应的视图(添加之前先要移除原来添加的视图)。这里要注意,这个布局是透明的。
看看lead_a的效果
到这里,视图就搞清楚了,那么我们来看看代码是如何控制速度的。
3. 设置和控制
我们在层级数中知道,第二层是一个横向滑动的FrameLayout。所以我们需要设置每个FrameLayout的大小
FrameLayout.LayoutParams framLayoutParams; ImageView layer_image_one = (ImageView) findViewById(R.id.layer_image_one); framLayoutParams = (FrameLayout.LayoutParams) layer_image_one.getLayoutParams(); framLayoutParams.height = dm.heightPixels; framLayoutParams.width = dm.widthPixels; layer_image_one.setLayoutParams(framLayoutParams);
其它几个图层实现方式一样。
当然,我们也需要设置一下最下层的背景。
ImageView back_image_one = (ImageView) findViewById(R.id.back_image_one); layoutParams = back_image_one.getLayoutParams(); layoutParams.height = dm.heightPixels; layoutParams.width = dm.widthPixels; back_image_one.setLayoutParams(layoutParams);
这个地方设置一个就OK啦,因为在这里我们的背景没有滑动过,当然是不是需要重新设置要看具体的需求是什么。
最关键的一点,给viewPager设置监听器,并在onPageScrolled中监听滑动并设置layer层元素的滑动,同时在onPageSelected中监听按钮的隐藏和显现。
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { float realOffset = Cubic.easeIn(positionOffset, 0, 1, 1); total_page = adapter.getCount(); float offset = (float) ((float) (position + realOffset) * 1.0 / total_page); int offsetPosition = (int) (backgroundWidth * offset); float layerRealOffset = Sine.easeIn(positionOffset, 0, 1, 1); float layerOffset = (float) ((float) (position + layerRealOffset) * 1.0 / total_page); int layerOffsetPosition = (int) (backgroundWidth * layerOffset); layer_srcollview.scrollTo(layerOffsetPosition, 0); } @Override public void onPageSelected(int position) { switch (position) { case 0: startBtn.setVisibility(View.GONE); break; case 1: startBtn.setVisibility(View.GONE); break; case 2: startBtn.setVisibility(View.GONE); break; case 3: startBtn.setVisibility(View.GONE); break; case 4: startBtn.setVisibility(View.VISIBLE); break; } } @Override public void onPageScrollStateChanged(int state) { } });
看看easnIn的实现
public static float easeIn(float t, float b, float c, float d) { return -c * (float) Math.cos(t / d * (Math.PI / 2)) + c + b; }
上面这个方法就是余弦波的3PI/2到2PI的波形状,
float layerOffset = (float) ((float) (position + layerRealOffset) * 1.0 / total_page);
再加上position([0,1]),layerOffset也就到了0到1的范围,波的形状是倒过来的cos波,开始的1/4段,这样波斜率(对应速度)也就越来越快,知道停止,这样就形成了我们上图看到的视差。
小红书的实现
先贴出目录结构
其中最关键的是ParallaxContainer(自定义布局)和ParallaxLayoutInflater(加载布局)
几个布局页面里面都是些ImageView,我们可以发现ImageView里面有了新的属性,(这里报红没有关系,可以正常编译运行的),一定不要忘记在根布局中添加
xmlns:app="http://schemas.android.com/apk/res-auto"
看看属性类:
public class ParallaxViewTag { protected int index; protected float xIn; protected float xOut; protected float yIn; protected float yOut; protected float alphaIn; protected float alphaOut; }
再看看attrs.xml文件
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="a_in" format="float" /> <attr name="a_out" format="float" /> <attr name="x_in" format="float" /> <attr name="x_out" format="float" /> <attr name="y_in" format="float" /> <attr name="y_out" format="float" /> </resources>
接下来看看小红书是怎么实现的:
if (mParallaxContainer != null) { mParallaxContainer.setImage(iv_man); mParallaxContainer.setLooping(false); iv_man.setVisibility(View.VISIBLE); mParallaxContainer.setupChildren(getLayoutInflater(), R.layout.view_intro_1, R.layout.view_intro_2, R.layout.view_intro_3, R.layout.view_intro_4, R.layout.view_intro_5, R.layout.view_login); }
设置行走的人,设置子布局。我们来看看setupChildren方法:
public void setupChildren(LayoutInflater inflater, int... childIds) { if (getChildCount() > 0) { throw new RuntimeException("setupChildren should only be called once when ParallaxContainer is empty"); } ParallaxLayoutInflater parallaxLayoutInflater = new ParallaxLayoutInflater( inflater, getContext()); for (int childId : childIds) { View view = parallaxLayoutInflater.inflate(childId, this); viewlist.add(view); } pageCount = getChildCount(); for (int i = 0; i < pageCount; i++) { View view = getChildAt(i); addParallaxView(view, i); } updateAdapterCount(); viewPager = new ViewPager(getContext()); viewPager.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT)); viewPager.setId(R.id.parallax_pager); attachOnPageChangeListener(); viewPager.setAdapter(adapter); addView(viewPager, 0); }
· 将布局页加载进来
· for (int childId : childIds) { View view = parallaxLayoutInflater.inflate(childId, this); viewlist.add(view); } pageCount = getChildCount(); for (int i = 0; i < pageCount; i++) { View view = getChildAt(i); addParallaxView(view, i); }
· 初始化ViewPager并设置监听
viewPager = new ViewPager(getContext()); viewPager.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT)); viewPager.setId(R.id.parallax_pager); attachOnPageChangeListener(); viewPager.setAdapter(adapter); addView(viewPager, 0);
我们再来看看addParallaxView的方法:
private void addParallaxView(View view, int pageIndex) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { addParallaxView(viewGroup.getChildAt(i), pageIndex); } } ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag); if (tag != null) { tag.index = pageIndex; parallaxViews.add(view); } }
这个方法就是用来添加子view的。
attachOnPageChangeListener方法源码有些长,我们就只看看关键的代码。就是初始化ViewPager.OnPageChangeListener监听器,并在onPageScrolled方法中根据我们的tag和偏移量来移动view。
for (View view : parallaxViews) { tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag); if (tag == null) { continue; } if ((pageIndex == tag.index - 1 || (isLooping && (pageIndex == tag.index - 1 + pageCount))) && containerWidth != 0) { // make visible view.setVisibility(VISIBLE); // slide in from right view.setTranslationX((containerWidth - offsetPixels) * tag.xIn); // slide in from top view.setTranslationY(0 - (containerWidth - offsetPixels) * tag.yIn); // fade in view.setAlpha(1.0f - (containerWidth - offsetPixels) * tag.alphaIn / containerWidth); } else if (pageIndex == tag.index) { // make visible view.setVisibility(VISIBLE); // slide out to left view.setTranslationX(0 - offsetPixels * tag.xOut); // slide out to top view.setTranslationY(0 - offsetPixels * tag.yOut); // fade out view.setAlpha(1.0f - offsetPixels * tag.alphaOut / containerWidth); } else { view.setVisibility(GONE); } }
这里频繁出现的Tag是什么时候设置的呢?仔细想想,应该是在加载的时候进行设置的,那我们看看setupChildren方法中有没有想过设置。发现如下代码:
ParallaxLayoutInflater parallaxLayoutInflater = new ParallaxLayoutInflater( inflater, getContext()); for (int childId : childIds) { View view = parallaxLayoutInflater.inflate(childId, this); viewlist.add(view); }
每个子view都是通过ParallaxLayoutInflater来加载的。那么我们看看ParallaxLayoutInflater是怎么做的,代码如下:
public class ParallaxLayoutInflater extends LayoutInflater { protected ParallaxLayoutInflater(LayoutInflater original, Context context){ super(original, context); setUpLayoutFactory(); } private void setUpLayoutFactory(){ if(!(getFactory() instanceof ParallaxFactory)){ setFactory(new ParallaxFactory(this,getFactory())); } } @Override public LayoutInflater cloneInContext(Context newContext) { return null; } }
我们可以发现构造方法中,通过setFactory方法,设置Factory为ParallaxFactory,到这里,就明白了。ParallaxFactory实现了LayoutInflater.Factory,这个接口有什么用。我们看看该接口的介绍:
public interface Factory { /** * Hook you can supply that is called when inflating from a LayoutInflater. * You can use this to customize the tag names available in your XML * layout files. * * <p> * Note that it is good practice to prefix these custom names with your * package (i.e., com.coolcompany.apps) to avoid conflicts with system * names. * * @param name Tag name to be inflated. * @param context The context the view is being created in. * @param attrs Inflation attributes as specified in XML file. * * @return View Newly created view. Return null for the default * behavior. */ public View onCreateView(String name, Context context, AttributeSet attrs); }
大概是说,我们可以通过实现这个接口来获取xml文件中的tag(就像app:x_in等等属性),由于ImageView不是我们的自定义控件,我们没法再ImageView的构造方法中获取一些值,所以通过实现这个接口来获取。
protected void onViewCreated(View view, Context context, AttributeSet attrs) { int[] attrIds = { R.attr.a_in, R.attr.a_out, R.attr.x_in, R.attr.x_out, R.attr.y_in, R.attr.y_out, }; TypedArray a = context.obtainStyledAttributes(attrs, attrIds); if (a != null) { if (a.length() > 0) { ParallaxViewTag tag = new ParallaxViewTag(); tag.alphaIn = a.getFloat(0, 0f); tag.alphaOut = a.getFloat(1, 0f); tag.xIn = a.getFloat(2, 0f); tag.xOut = a.getFloat(3, 0f); tag.yIn = a.getFloat(4, 0f); tag.yOut = a.getFloat(5, 0f); view.setTag(R.id.parallax_view_tag, tag); } a.recycle(); } }
这上面的方法中,我们获取了属性并给view设置了tag,所以,在后面我们就可以通过tag来获取这些值了。
接下来看看Adapter。
@Override public Object instantiateItem(ViewGroup container, int position) { View view; if (!recycleBin.isEmpty()) { view = recycleBin.pop(); } else { view = new View(context); //这里注意,如果不想加前缀,请以静态的方式将包和属性导进来 view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } container.addView(view); return view; }
什么都没有的空白页,我们的所有逻辑都在ParallaxFactory这个自定义Layout里面,图片什么的也都是,所以我们不需要ViewPager有什么东西(这里是跟ecmobiled的区别之一),我们的viewpager只负责滑动,只负责触发onPageScrolled方法,剩下的就是有view的setTranslationX、setTranslationY、setAlpha方法来实现。
HTML5实现引导页
使用HTML5制作引导页,对前端开发能力要求很高,当然我说的是实现炫酷效果的前提下,如果前端知识不足做出来的效果可能还赶不上普通引导页的效果,我承认我的前端开发能力就很差,所以我做出来的效果就比较丑,没有什么炫酷的动画在里面,当然只是借此效果来讲解HTML5实现引导页的方式,如果你的前端知识是在匮乏,你可以找公司的前端,让他帮助你实现一个狂炫酷拽吊炸天的效果,duang~~,好了不多说,先看看效果图:
声明:真正的HTML5实现的效果比图片上的效果好上很多倍,我只是简单的展示一下效果,吐槽什么的尽管释放,不要压抑心中的想法。
看看我的assets文件结构
首先我们分析一下,都需要哪些工作?
1、制作HTML5引导页。
2、把做好的页面放入Android工程中的assets文件夹下。
3、利用WebView加载assets文件夹下的html文件。
4、在引导页最后一页的按钮上捕捉点击事件,结束引导页,进入应用。
具体实现,首先看看xml布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="none"></WebView> </LinearLayout>
再来看看index.html的UI部分:
<head> <meta charset="utf-8"> <title>jQuery加CSS3 实现slide动画</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/> <meta name="apple-mobile-web-app-capable" content="yes"/> <link href="style.css" type="text/css" rel="stylesheet"/> <script src="js/jquery-2.1.4.min.js"></script> </head> <body> <div id="position-nav"></div> <div id="slides"> <div class="slide-list"></div> <div class="slide-list"></div> <div class="slide-list"></div> <div class="slide-list"> <button οnclick="window.open('http:start')">立即体验</button> </div> </div> </body>
我们可以发现在最后一个div中有一个按钮,按钮中添加了点击事件,我们就是通过webview和原生通过js交互捕捉按钮点击事件,具体的CSS和JS我就不讲了,看看文件中的源码,因为我也是让我们的前端大哥帮忙做的,当然前端大哥比较忙,我也不好意思太麻烦人家,就让他帮忙简单做了一下效果。
最后看看Activity中的实现:
public class Html5ForWelActivity extends Activity { private WebView webView; private String url; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_second); webView = (WebView) findViewById(R.id.webview); url = "file:///android_asset/index.html"; loadLocalHtml(url); } @SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled"}) private void loadLocalHtml(String url) { WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if ("http://start/".equals(url)) { // Intent intent = new Intent(Html5ForWelActivity.this, MainActivity.class); // startActivity(intent); finish(); } return super.shouldOverrideUrlLoading(view, url); } @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); } }); webView.loadUrl(url); } }
注意:不要忘了loadLocalHtml(String url)上面的注释,如果没有该注释js交互无效。
另外,当使用WebView浏览网页时,不做处理的话,按下手机的返回键会直接结束WebView所在的Activity,通过重写onKeyDown()方法,当WebView可以返回时,让其执行返回操作。
总结
通过上面的讲解,可以看出:
普通引导页,实现简单也是最常使用的一种方式,但是缺乏很多炫酷的效果,比较机械。
视差引导页,效果明显比普通引导页炫酷了很多,从技术实现角度来讲,对开发能力要求较高,而且视差引导页出在一个不上不下的地位,很容易被Html5取代。
HTML5引导页,HTML5对动画的完美支持很容易实现炫酷的引导效果,越是炫酷的效果就越是需要更高的前端开发能力。
感谢全世界-gl提供的普通引导页和视差引导页的讲解,参考:http://blog.csdn.net/qq_21430549/article/details/50066295
资源下载地址:http://download.csdn.net/detail/zhimingshangyan/9470174