Android通過(guò)自定義控件實(shí)現(xiàn)360軟件詳情頁(yè)效果
一、概述
最近有不少朋友私聊問(wèn)應(yīng)用寶、360軟件助手之類的軟件詳情頁(yè)怎么做,剛好,最近有時(shí)間就模仿360軟件助手詳情頁(yè)給大家做個(gè)Demo,供大家參考。嗯,關(guān)于實(shí)現(xiàn)呢,我寫了兩種方式:
1、ScrollView內(nèi)嵌軟件介紹+ViewPager+ViewPager中是ScrollView,這種方式呢,純?cè)瑳]有涉及到自定義控件,但是這樣嵌套呢,涉及到測(cè)量以及事件的沖突處理,大家可以自己嘗試去做一下,想像起來(lái)蠻容易的,做起來(lái)其實(shí)還是挺費(fèi)勁的,代碼我會(huì)給出,核心代碼不多,大家自行參考。本文將重點(diǎn)分析第二種方式。
2、將做外層的ScrollView改為了自定義的一個(gè)控件,繼承自LinearLayout,叫做StickyNavLayout,這里感謝小七的命名,同時(shí)本方式感謝二群暖暖提供的源碼。
最后看下效果圖,第一張是360的,第二張是我們的:
360:

擦,別問(wèn)我為什么這么模糊,盡力了。。。
我們的效果圖:

二、使用方式
上面我們也說(shuō)了,之所以有第二種方式,完全是為了考慮使用的容易度。
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>
定義了幾個(gè)id資源,主要是為了方便使用了,供聲明布局時(shí)使用的,看名字應(yīng)該能猜出來(lái)吧,猜不出來(lái)沒事,接下來(lái)我就貼布局文件了。這個(gè)其實(shí)不屬于使用方式了,但是下文會(huì)見到,所以提前貼出來(lái)。
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,然后是頂部?jī)?nèi)容區(qū)域,Vp的指示器,ViewPager。按照效果圖,去寫就ok,注意部分id使用我們預(yù)設(shè)定的id資源。因?yàn)槲覀兊腟tickyNavLayout需要通過(guò)id找到該控件,去進(jìn)行一些計(jì)算。
然后在我們的MainActivity中,對(duì)ViewPager進(jìn)行初始化即可。
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[] { "簡(jiǎn)介", "評(píng)價(jià)", "相關(guān)" };
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);
}
}
沒有什么復(fù)雜的代碼,主要就是初始化我們的Vp;
對(duì)了這個(gè)指示器我是臨時(shí)寫的,也算一個(gè)自定義控件吧,主要就是跟隨Vp一起移動(dòng),只是把三角形改成了下劃線。
我們的Vp中每個(gè)頁(yè)面為一個(gè)Fragment,F(xiàn)ragment的代碼我們就不貼了,布局就是ScrollView為根布局,內(nèi)部隨意填充,具體可參考源碼。
介紹完了用法有木有一點(diǎn)小激動(dòng),基本按照常規(guī)去寫布局就ok,效果自動(dòng)實(shí)現(xiàn)。
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> //省略了無(wú)數(shù)控件 </LinearLayout> </ScrollView>
沒撒說(shuō)的 ,let's go 。
三、StickyNavLayout源碼剖析
1、構(gòu)造
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中完成,可以看到直接通過(guò)我們的id資源讀取就ok。接下來(lái)還有些mScroller、mVelocityTracker、mTouchSlop、mMaximumVelocity、mMinimumVelocity、mLastY、mDragging,不用說(shuō)大家都能想到這是和移動(dòng)相關(guān)的,OverScroller是個(gè)輔助類,用于自定移動(dòng)時(shí)幫我們處理掉數(shù)學(xué)的計(jì)算部分。mVelocityTracker相關(guān)幾個(gè)變量,當(dāng)然是計(jì)算什么時(shí)候需要自動(dòng)移動(dòng);mTouchSlop幫我區(qū)別用戶是點(diǎn)擊還是拖拽。Android中封裝了很多常量在ViewConfiguration中,大家有興趣可以了解他,之所以使用這些常量不僅僅是說(shuō),省的我們自己去定義,而是為了和系統(tǒng)的行為保持一致。
看完了構(gòu)造以后,由于我們使用的是LinearLayout,直接setOrientation(LinearLayout.VERTICAL);也就不必去layout了,控件都自定垂直排列了。那么我們?cè)趏nMeasure中還需要做些處理。
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();
}
主要是去設(shè)置ViewPager的高度,給它一個(gè)固定值,ViewPager自己在測(cè)量自己的時(shí)候,你要是不給它固定值,可能測(cè)量結(jié)果與你的預(yù)期會(huì)差距很大。比如你給它設(shè)置個(gè)WRAP_CONTENT,你希望他去計(jì)算孩子的高度去設(shè)置自己的,那么你就想多了。
大家可以看下ViewPager測(cè)量的源碼:
@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;
}
可以看到,對(duì)于AT_MOST和EXACTLY兩種模式,都是直接讀取父類的傳入的測(cè)量值,也就是說(shuō),他不會(huì)去測(cè)量自己孩子的高度。然后如果模式是:UNSPECIFIED,那么高度直接為0呢。這里大家如果做過(guò)這個(gè)例子,應(yīng)該能遇到這種情況,ScrollView中放ViewPager時(shí),測(cè)量模式就是UNSPECIFIED,那么Vp直接不顯示,原因就在這里。
扯遠(yuǎn)了,回來(lái),我們繼續(xù)。
我們?cè)O(shè)置為Vp的值以后,理論上來(lái)說(shuō),我們的顯示已經(jīng)正常了,控件都按照我們的預(yù)期顯示了,但是,但是什么呢?我們現(xiàn)在在自定義的LinearLayout,那么移動(dòng)是不是應(yīng)該我們自己去寫。
移動(dòng)的代碼很簡(jiǎn)單,想必大家都知道,直接拿到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);
}
比較簡(jiǎn)單哈,我們因?yàn)橹恍枰袛鄖方向的,所以down的時(shí)候記錄下y的值,然后move的時(shí)候拿到dy,直接去進(jìn)去scrollBy就好,當(dāng)然我們?cè)谡麄€(gè)過(guò)程中.addMovement(event);所以,up的時(shí)候,我們得到v方向的velocityY,調(diào)用fling進(jìn)行移動(dòng)。
還好fling的核心代碼OverScroller給我們實(shí)現(xiàn)了,so nice。
大家應(yīng)該清楚,我們使用Scroller這樣的輔助類時(shí),它們幫我們完成的,知識(shí)數(shù)學(xué)方面的計(jì)算,至于自動(dòng)我們還是需要自己去干的。
那怎么干,在哪干?這個(gè)無(wú)非就是重寫computeScroll方法,在里面判斷scroller是否結(jié)束,如果沒有,則scrollTo一下,最后記得invalidate,相關(guān)代碼:
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,別得意,為什么這么說(shuō)呢?因?yàn)槟阃瓿傻模R(shí)當(dāng)然View對(duì)于上下拖動(dòng)的處理。大家別忘了,我們當(dāng)前的StickyNavLayout內(nèi)部可是有一個(gè)ScrollView的,那么根據(jù)事件的轉(zhuǎn)發(fā)機(jī)制,這個(gè)內(nèi)部的ScrollView肯定會(huì)處理上下拖動(dòng)這種情況的,也就是我們的事件會(huì)被它攔截。
4、onInterceptTouchEvent
好了,接下來(lái)我們要處理攔截,對(duì)于攔截,我們要清楚的知道什么時(shí)候應(yīng)該攔截,什么時(shí)候不需要,當(dāng)前我們的例子:
1、如果我們的頂部view只要沒有完全隱藏,那么直接攔截上下的拖動(dòng);
2、還有個(gè)需要攔截的地方,就是當(dāng)頂部的view徹底隱藏了,我們現(xiàn)在內(nèi)部的sc應(yīng)該可以上下滑動(dòng)了,但是如果sc滑動(dòng)到頂部再往下的時(shí)候,此時(shí)又該攔截了,我們需要把頂部view可以下滑出來(lái)。
分析完成以后,看代碼,這叫一個(gè)酸爽:
@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
本地下載地址:http://xiazai.jb51.net/201705/yuanma/Android-StickyNavLayout(jb51.net).rar
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android中Progress的簡(jiǎn)單實(shí)例
- Android 屬性動(dòng)畫ValueAnimator與插值器詳解
- Android中Edittext設(shè)置輸入條件
- 詳解Android中的NestedScrolling機(jī)制帶你玩轉(zhuǎn)嵌套滑動(dòng)
- Android SDK Manager更新、下載速度慢問(wèn)題解決辦法
- Android 使用<layer-list>實(shí)現(xiàn)微信聊天輸入框功能
- android中強(qiáng)制更新app實(shí)例代碼
- Android自定義view實(shí)現(xiàn)太極效果實(shí)例代碼
相關(guān)文章
大型項(xiàng)目里Flutter測(cè)試應(yīng)用實(shí)例集成測(cè)試深度使用詳解
這篇文章主要為大家介紹了大型項(xiàng)目里Flutter測(cè)試應(yīng)用實(shí)例集成測(cè)試深度使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Android中的廣播(BroadCast)詳細(xì)介紹
這篇文章主要介紹了Android中的廣播(BroadCast)詳細(xì)介紹,本文講解了什么是廣播、廣播有什么用、實(shí)現(xiàn)廣播、動(dòng)態(tài)注冊(cè)方式、配置文件方式等內(nèi)容,需要的朋友可以參考下2015-03-03
使用Android原生WebView+Highcharts實(shí)現(xiàn)可左右滑動(dòng)的折線圖
折線圖是Android開發(fā)中經(jīng)常會(huì)碰到的效果,但由于涉及自定義View的知識(shí),對(duì)許多剛?cè)腴T的小白來(lái)說(shuō)會(huì)覺得很高深,下面這篇文章主要給大家介紹了關(guān)于如何使用Android原生WebView+Highcharts實(shí)現(xiàn)可左右滑動(dòng)的折線圖的相關(guān)資料,需要的朋友可以參考下2022-05-05
Android實(shí)現(xiàn)手機(jī)定位的案例代碼
今天小編就為大家分享一篇關(guān)于Android實(shí)現(xiàn)手機(jī)定位的案例代碼,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
Android編程實(shí)現(xiàn)修改標(biāo)題欄位置使其居中的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)修改標(biāo)題欄位置使其居中的方法,涉及Android布局設(shè)置的簡(jiǎn)單實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Android基礎(chǔ)之隱藏標(biāo)題欄/設(shè)置為全屏/橫豎屏切換
大家好,本篇文章主要講的是Android基礎(chǔ)之隱藏標(biāo)題欄/設(shè)置為全屏/橫豎屏切換,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12
Android Activity的生命周期與啟動(dòng)模式全面解讀
雖然說(shuō)我們天天都在使用Activity,但是你真的對(duì)Activity的生命機(jī)制完全了解了嗎?Activity的生命周期方法只有七個(gè),但是其實(shí)那只是默認(rèn)的情況。也就是說(shuō)在其他情況下,Activity的生命周期可能不會(huì)是按照我們以前所知道的流程,這就要說(shuō)到Activity的啟動(dòng)模式2021-10-10

