編輯:關於Android編程
現在的一些購物類App例如淘寶,京東等,在物品詳情頁,都采用了類似分層的模式,即上拉加載詳情的方式,節省了空間,使用戶的體驗更加的舒適。只要對於某個東西的介紹很多時,都可以采取這樣的方式,第一個頁面顯示必要的,第二個頁面顯示詳細信息。
在之前寫項目的時候,曾經寫過一個類似淘寶詳情頁的UI效果,如下:
仔細分析這種UI效果,其實很簡單,就是兩個頁面,垂直擺放,同時兩個頁面之間過渡時,加上一層特殊處理,及當第二個頁面顯示多少時,松開手指時復原或者直接顯示第二個頁面。
根據這種特殊的UI效果進行實現封裝,最終的效果如下:
能夠實現頁面的切換,當滑動到第二個頁面不足顯示區域的三分之一時,則松開手指時會還原,如果超過三分之一,則會跳到第二個頁面。
同時實現了一些事件分發的處理,能夠嵌入按鈕,ViewPager等控件。
編寫xml文件,添加控件
在實現中,一定要將控件的高度設置為match_parent,因為在代碼中需要獲取顯示區域的高度,用以判斷是否復原和跳轉頁面。
在該控件中添加兩個布局控件。
在Activity中查找控件並設置一些監聽。
public class DetailVerticalActivity extends AppCompatActivity { private DetailVerticalView v; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_taobao_detail); // 查找控件 v = (DetailVerticalView) findViewById(R.id.detailVerticalView); // 設置滾動監聽的回調 v.setScrollChangeListener(new DetailVerticalView.ScrollChangeListener() { @Override public void scrollY(int y) { // y軸滑動偏移量的回調 } @Override public void onScollStateChange(int type) { // 滑動到那個頁面狀體的變化 if(DetailVerticalView.TOP == type){ Toast.makeText(DetailVerticalActivity.this, "1", Toast.LENGTH_SHORT).show(); }else if(DetailVerticalView.BOTTOM==type){ Toast.makeText(DetailVerticalActivity.this, "2", Toast.LENGTH_SHORT).show(); } } }); } public void test(View view){ Toast.makeText(getApplicationContext(),"點擊",Toast.LENGTH_SHORT).show(); } }
在實現過程中,主要考慮以下幾點問題:
如何布置兩個頁面的控件位置。解決方式是使自定義View實現ViewGroup,重寫onMeasure和onLayout方法,對子View進行布局。
如何判斷兩個頁面何時跳轉,解決方式:獲取View顯示區域高度的三分之一,如果超過,則顯示第二個頁面,否則
則恢復原狀,不顯示第二個頁面。
解決方式:使用Scroller進行滑動的控制。
判斷的一些邊界的問題。滑動到底部和滑動到頂部時,等不可再滑,需要對邊界進行配置。
事件分發的處理。對於點擊等事件,利用onInterceptTouchEvent() 對相關事件進行攔截。
創建對象,初始化控件
/** * * 仿淘寶詳情頁 * Created by alex_mahao on 2016/8/29. */ public class DetailVerticalView extends ViewGroup { // 滑動到頂部的狀態 public static final int TOP = 1; // 滑動到底部的狀態 public static final int BOTTOM = 2; public static final String TAG = "DetailVerticalView"; /** * 屏幕高度 */ private int mScreenHeight; /** * 手指上次觸摸事件的y軸位置 */ private int mLastY; /** * 點擊時y軸的偏移量 */ private int mScrollY; /** * 滾動控件 */ private Scroller mScroller; /** * 最小移動距離判定 */ private int mTouchSlop; /** * 滑動結束的偏移量 */ private int mEnd; /** * 初始按下y軸坐標 */ private int mDownStartY; /** * 記錄當前y軸坐標 */ private int y; /** * 控件的高度 */ private int mHeight; /** * 監聽的回調 */ private ScrollChangeListener scrollChangeListener; public DetailVerticalView(Context context) { super(context, null); } public DetailVerticalView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * 初始化方法 */ private void init() { // 創建滑對象,以便滑動時使用 mScroller = new Scroller(getContext()); // 獲取系統的最小距離 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(getContext())); } //.... }
一些變量的定義後面會有,此處不提。
mTouchSlop,系統默認的最小距離。當手指滑動的大小大於該值時,則認為是滑動,不在是點擊,後面會通過與該值比對進行事件攔截。
測量自身的大小和兩個頁面控件的大小
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 獲取顯示區域的高度 mScreenHeight = MeasureSpec.getSize(heightMeasureSpec); // 遍歷子View int count = getChildCount(); // 控件的高度 mHeight = 0; for (int i = 0; i < count; i++) { View childView = getChildAt(i); // 測量子View 高度 int childHeight = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); measureChild(childView, widthMeasureSpec, childHeight); mHeight = getChildAt(i).getMeasuredHeight() + mHeight; } // 設置控件的高度 setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY)); }
設置自定義View的高度為match_parent,就是為了獲取顯示區域的高度,從此處可以看出。
測量子View的高度,便於後面的布局使用。在中間,會看到我對childView的高度設置了值childHeight,該值的目的是告訴子View,我給你一個很大的值,你看著自己需要多少自己設置就行,及AT_MOST模式
在最後的時候,設置當前控件的高度,為兩個頁面控件的高度之和。
對兩個頁面控件進行布置onLayout()方法
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int childHeight = 0; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { // 確定位置 child.layout(l, childHeight, r, childHeight + child.getMeasuredHeight()); childHeight = +child.getMeasuredHeight(); } } }
這一段沒什麼難度,看著理解即可。
處理事件分發
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 獲取當前觸摸位置Y軸坐標 y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // 第一次按下時的Y軸坐標 mDownStartY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: // 如果大於mTouchSlop,認為是滑動,則不再讓子View處理事件 if (Math.abs(y - mDownStartY) > mTouchSlop) { // 因為是在onTouchEvent中處理,如果子View處理過事件, // 則該控件的onTouchEvent()不再有DOWN事件,在這裡獲取一些值 mLastY = y; mScrollY = getScrollY(); return true; } break; case MotionEvent.ACTION_UP: break; } return false; }
對於事件的傳遞,流程為 父:onInterceptTouchEventr - > 子:onTouchEvent() -> 父:onTouchEvent(),當然這裡只是寫了必要的流程。如果子:onTouchEvent()返回true,則當前控件就無法捕獲觸摸事件,那麼滑動就無從處理了。所以,在此處,我們判斷垂直滑動大於最小滑動距離時,就把事件給截斷,不在交給子控件處理。
同時,如果子控件處理了一些事件,那麼父控件的onTouchEvent()中,將不在有DOWN事件,那麼我們需要先獲取一次值,在打斷子View的時候。
滑動相關的處理
@Override public boolean onTouchEvent(MotionEvent event) { y = (int) event.getY(); mScrollY = getScrollY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 如果通過事件攔截獲取到的觸摸,則直接就為Move方法 mLastY = y; mScrollY = getScrollY(); break; case MotionEvent.ACTION_MOVE: int dy = mLastY - y; Log.i(TAG,"dy:"+dy+"-"+"mLastY:"+mLastY+"-"+"mScrolly:"+mScrollY); if(mScrollY+dy<0){ // 滑動到頂部,不可再滑動 scrollTo(0,0); }else if(mScrollY+dy>getMeasuredHeight()-mScreenHeight){ //底部時 scrollTo(0,getMeasuredHeight()-mScreenHeight); }else{ scrollBy(0, dy); mLastY = y; } break; case MotionEvent.ACTION_UP: // 當前偏移量是 int absScroll = mScrollY+mScreenHeight-getChildAt(0).getMeasuredHeight(); if(absScroll<0||absScroll>mScreenHeight){ // 第一個頁面未滑到底部,不做操作,如果absScroll>屏幕的高度,則完全滾動 // 不做任何滾動操作 }else if(absScroll>mScreenHeight/3){ // 監聽的回調 if(scrollChangeListener!=null){ scrollChangeListener.onScollStateChange(BOTTOM); } // 松開時顯示第二個頁面 mScroller.startScroll(0, mScrollY, 0, mScreenHeight-absScroll); }else if(absScroll<mscreenheight pre="" return="">
該段是整個自定義View中最重要的方法,總結來說干了兩件事情:
ACTION_MOVE中,處理觸摸滑動的事件,及手指在屏幕滑動時的相關處理。邊界處理,判斷滑動時,如果到頂部和底部,則不再滑動,否則進行相關的滑動。
ACTION_UP:手指離開屏幕時,對是否需要跳轉進行判斷,如果需要跳轉,則跳轉。在手指離開時,判斷當前頁面的顯示量,偏移量+顯示區域的高度-第一個控件的高度=第二個控件顯示的高度。
如果顯示的高度小於0,則表示還在第一個頁面,第二個頁面顯示都沒顯示,不做任何處理。
如果顯示的高度大於顯示區域的高度,則表示第二個頁面完全顯示了,不做任何處理。
否則,如果顯示的高度大於顯示區域的1/3,則跳轉到第二個頁面,小於,則恢復到第一個頁面。
可以看到通過mScroller.startScroll()實現頁面的滑動跳轉,使用該方式,需要重寫另一個方法
@Override public void computeScroll() { super.computeScroll(); //判斷scroller滾動是否完成 if (mScroller.computeScrollOffset()) { // 這裡調用View的scrollTo()完成實際的滾動 scrollTo(0, mScroller.getCurrY()); //刷新試圖 postInvalidate(); } }
最後一步,設置一些必要的監聽回調,和輔助方法
/** * 監聽的接口定義 */ public interface ScrollChangeListener{ void scrollY(int y); void onScollStateChange(int type); } /** * 設置監聽 * @param scrollChangeListener */ public void setScrollChangeListener(ScrollChangeListener scrollChangeListener) { this.scrollChangeListener = scrollChangeListener; }
/** * 回到第一個頁面的頂部 */ public void scrollToTop(){ mScroller.startScroll(0, getScrollY(), 0, -getScrollY()); scrollChangeListener.onScollStateChange(TOP); postInvalidate(); }
在ios7中有一種扁平風格的控件叫做分段選擇控件UISegmentedControl,控件上橫放或豎放著幾個被簡單線條隔開的按鈕,每次點擊能切換不同的按鈕和按鈕所對應的界
一、Android事件現代的用戶界面,都是以事件來驅動的來實現人機交換的。而Android上的一套UI控件,無非就是派發鼠標和鍵盤事件,然後每個控件收到相應的事件之後,做
哈哈,這種需求我也是醉了。今天有個搞ios的朋友(以前公司同事,現在是Leader)問我他們公司安卓要做版本升級,然後簽名文件有但是password 和 alias忘記
最近編程時,發現一個針對HashMap的一個提示:翻譯過來就是:用SparseArray來代替會有更好性能。那我們就來看看源碼中SparseArray到底做了哪些事情:一