一、概述
最近有不少朋友私聊问应用宝、360软件助手之类的软件详情页怎么做,刚好,最近有时间就模仿360软件助手详情页给大家做个demo,供大家参考。嗯,关于实现呢,我写了两种方式:
1、scrollview内嵌软件介绍+viewpager+viewpager中是scrollview,这种方式呢,纯原生,没有涉及到自定义控件,但是这样嵌套呢,涉及到测量以及事件的冲突处理,大家可以自己尝试去做一下,想像起来蛮容易的,做起来其实还是挺费劲的,代码我会给出,核心代码不多,大家自行参考。本文将重点分析第二种方式。
2、将做外层的scrollview改为了自定义的一个控件,继承自linearlayout,叫做stickynavlayout,这里感谢小七的命名,同时本方式感谢二群暖暖提供的源码。
最后看下效果图,第一张是360的,第二张是我们的:
360:
擦,别问我为什么这么模糊,尽力了。。。
我们的效果图:
二、使用方式
上面我们也说了,之所以有第二种方式,完全是为了考虑使用的容易度。
1、自定义id资源文件
values/ids_sticky_nav_llayout.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="id_stickynavlayout_topview" type="id"/> <item name="id_stickynavlayout_viewpager" type="id"/> <item name="id_stickynavlayout_indicator" type="id"/> <item name="id_stickynavlayout_innerscrollview" type="id"/> </resources>
定义了几个id资源,主要是为了方便使用了,供声明布局时使用的,看名字应该能猜出来吧,猜不出来没事,接下来我就贴布局文件了。这个其实不属于使用方式了,但是下文会见到,所以提前贴出来。
2、布局文件
<com.zhy.view.stickynavlayout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <relativelayout android:id="@id/id_stickynavlayout_topview" android:layout_width="match_parent" android:layout_height="300dp" android:background="#4400ff00" > <textview android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="软件介绍" android:textsize="30sp" android:textstyle="bold" /> </relativelayout> <com.zhy.view.simpleviewpagerindicator android:id="@id/id_stickynavlayout_indicator" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffffffff" > </com.zhy.view.simpleviewpagerindicator> <android.support.v4.view.viewpager android:id="@id/id_stickynavlayout_viewpager" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#44ff0000" > </android.support.v4.view.viewpager> </com.zhy.view.stickynavlayout>
最外层是我们的自定义的控件stickynavlayout,然后是顶部内容区域,vp的指示器,viewpager。按照效果图,去写就ok,注意部分id使用我们预设定的id资源。因为我们的stickynavlayout需要通过id找到该控件,去进行一些计算。
然后在我们的mainactivity中,对viewpager进行初始化即可。
3、mainactivity
package com.zhy.sample.stickynavlayout; import android.os.bundle; import android.support.v4.app.fragment; import android.support.v4.app.fragmentactivity; import android.support.v4.app.fragmentpageradapter; import android.support.v4.view.viewpager; import android.support.v4.view.viewpager.onpagechangelistener; import com.zhy.view.simpleviewpagerindicator; public class mainactivity extends fragmentactivity { private string[] mtitles = new string[] { "简介", "评价", "相关" }; private simpleviewpagerindicator mindicator; private viewpager mviewpager; private fragmentpageradapter madapter; private tabfragment[] mfragments = new tabfragment[mtitles.length]; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); initviews(); initdatas(); initevents(); } private void initevents() { mviewpager.setonpagechangelistener(new onpagechangelistener() { @override public void onpageselected(int position) { } @override public void onpagescrolled(int position, float positionoffset, int positionoffsetpixels) { mindicator.scroll(position, positionoffset); } @override public void onpagescrollstatechanged(int state) { } }); } private void initdatas() { mindicator.settitles(mtitles); for (int i = 0; i < mtitles.length; i++) { mfragments[i] = (tabfragment) tabfragment.newinstance(mtitles[i]); } madapter = new fragmentpageradapter(getsupportfragmentmanager()) { @override public int getcount() { return mtitles.length; } @override public fragment getitem(int position) { return mfragments[position]; } }; mviewpager.setadapter(madapter); mviewpager.setcurrentitem(0); } private void initviews() { mindicator = (simpleviewpagerindicator) findviewbyid(r.id.id_stickynavlayout_indicator); mviewpager = (viewpager) findviewbyid(r.id.id_stickynavlayout_viewpager); } }
没有什么复杂的代码,主要就是初始化我们的vp;
对了这个指示器我是临时写的,也算一个自定义控件吧,主要就是跟随vp一起移动,只是把三角形改成了下划线。
我们的vp中每个页面为一个fragment,fragment的代码我们就不贴了,布局就是scrollview为根布局,内部随意填充,具体可参考源码。
介绍完了用法有木有一点小激动,基本按照常规去写布局就ok,效果自动实现。
4、fragment及其布局
贴一下我们的fragment代码:
package com.zhy.sample.stickynavlayout; import android.os.bundle; import android.support.v4.app.fragment; import android.view.layoutinflater; import android.view.view; import android.view.viewgroup; import android.widget.textview; public class tabfragment extends fragment { public static final string title = "title"; private string mtitle = "defaut value"; private textview mtextview; @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); if (getarguments() != null) { mtitle = getarguments().getstring(title); } } @override public view oncreateview(layoutinflater inflater, viewgroup container, bundle savedinstancestate) { view view = inflater.inflate(r.layout.fragment_tab, container, false); mtextview = (textview) view.findviewbyid(r.id.id_info); mtextview.settext(mtitle); return view; } public static tabfragment newinstance(string title) { tabfragment tabfragment = new tabfragment(); bundle bundle = new bundle(); bundle.putstring(title, title); tabfragment.setarguments(bundle); return tabfragment; } }
<scrollview xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@id/id_stickynavlayout_innerscrollview" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="none" > <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#eee" android:orientation="vertical" android:padding="5dp" > <textview android:id="@+id/id_info" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffffffff" android:gravity="center" > </textview> //省略了无数控件 </linearlayout> </scrollview>
没撒说的 ,let's go 。
三、stickynavlayout源码剖析
1、构造
public class stickynavlayout extends linearlayout { private view mtop; private view mnav; private viewpager mviewpager; private int mtopviewheight; private scrollview minnerscrollview; private boolean istophidden = false; private overscroller mscroller; private velocitytracker mvelocitytracker; private int mtouchslop; private int mmaximumvelocity, mminimumvelocity; private float mlasty; private boolean mdragging; public stickynavlayout(context context, attributeset attrs) { super(context, attrs); setorientation(linearlayout.vertical); mscroller = new overscroller(context); mvelocitytracker = velocitytracker.obtain(); mtouchslop = viewconfiguration.get(context).getscaledtouchslop(); mmaximumvelocity = viewconfiguration.get(context) .getscaledmaximumflingvelocity(); mminimumvelocity = viewconfiguration.get(context) .getscaledminimumflingvelocity(); } @override protected void onfinishinflate() { super.onfinishinflate(); mtop = findviewbyid(r.id.id_stickynavlayout_topview); mnav = findviewbyid(r.id.id_stickynavlayout_indicator); view view = findviewbyid(r.id.id_stickynavlayout_viewpager); if (!(view instanceof viewpager)) { throw new runtimeexception( "id_stickynavlayout_viewpager show used by viewpager !"); } mviewpager = (viewpager) view; }
ok,首先看下成员变量,和我们的变量的初始化,mtop、mnav、mviewpager代表我们布局的三大块了,初始化在onfinishinflate中完成,可以看到直接通过我们的id资源读取就ok。接下来还有些mscroller、mvelocitytracker、mtouchslop、mmaximumvelocity、mminimumvelocity、mlasty、mdragging,不用说大家都能想到这是和移动相关的,overscroller是个辅助类,用于自定移动时帮我们处理掉数学的计算部分。mvelocitytracker相关几个变量,当然是计算什么时候需要自动移动;mtouchslop帮我区别用户是点击还是拖拽。android中封装了很多常量在viewconfiguration中,大家有兴趣可以了解他,之所以使用这些常量不仅仅是说,省的我们自己去定义,而是为了和系统的行为保持一致。
看完了构造以后,由于我们使用的是linearlayout,直接setorientation(linearlayout.vertical);
也就不必去layout了,控件都自定垂直排列了。那么我们在onmeasure中还需要做些处理。
2、onmeasure
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); viewgroup.layoutparams params = mviewpager.getlayoutparams(); params.height = getmeasuredheight() - mnav.getmeasuredheight(); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); mtopviewheight = mtop.getmeasuredheight(); }
主要是去设置viewpager的高度,给它一个固定值,viewpager自己在测量自己的时候,你要是不给它固定值,可能测量结果与你的预期会差距很大。比如你给它设置个wrap_content,你希望他去计算孩子的高度去设置自己的,那么你就想多了。
大家可以看下viewpager测量的源码:
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { // for simple implementation, our internal size is always 0. // we depend on the container to specify the layout size of // our view. we can't really know what it is since we will be // adding and removing different arbitrary views and do not // want the layout to change as this happens. setmeasureddimension(getdefaultsize(0, widthmeasurespec), getdefaultsize(0, heightmeasurespec)); } public static int getdefaultsize(int size, int measurespec) { int result = size; int specmode = measurespec.getmode(measurespec); int specsize = measurespec.getsize(measurespec); switch (specmode) { case measurespec.unspecified: result = size; break; case measurespec.at_most: case measurespec.exactly: result = specsize; break; } return result; }
可以看到,对于at_most和exactly两种模式,都是直接读取父类的传入的测量值,也就是说,他不会去测量自己孩子的高度。然后如果模式是:unspecified,那么高度直接为0呢。这里大家如果做过这个例子,应该能遇到这种情况,scrollview中放viewpager时,测量模式就是unspecified,那么vp直接不显示,原因就在这里。
扯远了,回来,我们继续。
我们设置为vp的值以后,理论上来说,我们的显示已经正常了,控件都按照我们的预期显示了,但是,但是什么呢?我们现在在自定义的linearlayout,那么移动是不是应该我们自己去写。
移动的代码很简单,想必大家都知道,直接拿到dx,然后scrollby就行了。
3、ontouchevent
@override public boolean ontouchevent(motionevent event) { mvelocitytracker.addmovement(event); int action = event.getaction(); float y = event.gety(); switch (action) { case motionevent.action_down: if (!mscroller.isfinished()) mscroller.abortanimation(); mvelocitytracker.clear(); mvelocitytracker.addmovement(event); mlasty = y; return true; case motionevent.action_move: float dy = y - mlasty; if (!mdragging && math.abs(dy) > mtouchslop) { mdragging = true; } if (mdragging) { scrollby(0, (int) -dy); mlasty = y; } break; case motionevent.action_cancel: mdragging = false; if (!mscroller.isfinished()) { mscroller.abortanimation(); } break; case motionevent.action_up: mdragging = false; mvelocitytracker.computecurrentvelocity(1000, mmaximumvelocity); int velocityy = (int) mvelocitytracker.getyvelocity(); if (math.abs(velocityy) > mminimumvelocity) { fling(-velocityy); } mvelocitytracker.clear(); break; } return super.ontouchevent(event); }
比较简单哈,我们因为只需要判断y方向的,所以down的时候记录下y的值,然后move的时候拿到dy,直接去进去scrollby就好,当然我们在整个过程中.addmovement(event);
所以,up的时候,我们得到v方向的velocityy,调用fling进行移动。
还好fling的核心代码overscroller给我们实现了,so nice。
大家应该清楚,我们使用scroller这样的辅助类时,它们帮我们完成的,知识数学方面的计算,至于自动我们还是需要自己去干的。
那怎么干,在哪干?这个无非就是重写computescroll方法,在里面判断scroller是否结束,如果没有,则scrollto一下,最后记得invalidate,相关代码:
public void fling(int velocityy) { mscroller.fling(0, getscrolly(), 0, velocityy, 0, 0, 0, mtopviewheight); invalidate(); } @override public void scrollto(int x, int y) { if (y < 0) { y = 0; } if (y > mtopviewheight) { y = mtopviewheight; } if (y != getscrolly()) { super.scrollto(x, y); } istophidden = getscrolly() == mtopviewheight; } @override public void computescroll() { if (mscroller.computescrolloffset()) { scrollto(0, mscroller.getcurry()); invalidate(); } }
ok,到此,我们的ontouchevent搞定了~~but,别得意,为什么这么说呢?因为你完成的,知识当然view对于上下拖动的处理。大家别忘了,我们当前的stickynavlayout内部可是有一个scrollview的,那么根据事件的转发机制,这个内部的scrollview肯定会处理上下拖动这种情况的,也就是我们的事件会被它拦截。
4、onintercepttouchevent
好了,接下来我们要处理拦截,对于拦截,我们要清楚的知道什么时候应该拦截,什么时候不需要,当前我们的例子:
1、如果我们的顶部view只要没有完全隐藏,那么直接拦截上下的拖动;
2、还有个需要拦截的地方,就是当顶部的view彻底隐藏了,我们现在内部的sc应该可以上下滑动了,但是如果sc滑动到顶部再往下的时候,此时又该拦截了,我们需要把顶部view可以下滑出来。
分析完成以后,看代码,这叫一个酸爽:
@override public boolean onintercepttouchevent(motionevent ev) { int action = ev.getaction(); float y = ev.gety(); switch (action) { case motionevent.action_down: mlasty = y; break; case motionevent.action_move: float dy = y - mlasty; getcurrentscrollview(); if (math.abs(dy) > mtouchslop) { mdragging = true; if (!istophidden || (minnerscrollview.getscrolly() == 0 && istophidden && dy > 0)) { return true; } } break; } return super.onintercepttouchevent(ev); }
ok,move中判断上述两种情况,o了。
源码下载:
github下载地址:https://github.com/hongyangandroid/android-stickynavlayout
本地下载地址:
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。
如对本文有疑问, 点击进行留言回复!!
LongClick原理、上下文菜单原理、EditText长按弹窗原理、WebView长按弹窗自定义、修复WebView全选重复bug ———————————————— 版权声明:本文为CSDN博主「
JobScheduler 实现 特定时间,特定条件(系统空闲,电池电量,磁盘空间 ……)下执行任务
温习Android基础知识——《第一行代码(第三版)》读书笔记 Chapter 10 Service
网友评论