android 自定義ScrollView實現背景圖片伸縮的實現程式碼及思路
首先還是按照慣例給大家看下示例.
用過多米音樂的都會知道, 這個UI可以上下滑動,作用嘛---無聊中可以劃劃解解悶,這被錘子公司老羅稱呼為“情懷”,其實叫“情趣”更合適。嘿嘿.如今移動網際網路發展這麼迅速,市場上已不再是那初期隨便敲個APP放上架就能擁有幾十萬使用者的階段了.最近蘋果公司,為了怕android下載量趕超蘋果商店,大勢聲稱:(第 500 億個下載應用的使用者就可以獲得 10,000 美元的 iTunes 禮品卡,除此之外,緊隨第 500 億之後的前 50 名使用者也可以獲得 500 美元的禮品卡.至於移動發展趨勢,我想搞移動IT的人心裡都比較清楚,扯遠了).其實應用UI特效是應用中很大的一部分,如果同樣功能的兩款軟體,一個功能好點如“網易新聞”,另外一個稍微差點如“新浪新聞”,使用者的你毫無疑問肯定會選擇網易客戶端.總結就是“操作性”對於產品起著至關重要的因素.
接下來我們看下如何實現,首先宣告,這個實現的方式不是很好,我這裡只是提出一個解決方案,大家可以根據自己的想法進行創新.
原理:RelativeLayout+自定義ScrollView.
我們大致看下佈局結構如圖:
其實也沒什麼技術含量,我簡單介紹下:紅色代表的是背景照片,綠色的代表自定義ScrollView,粉色是代表你要編輯的透明區域.也不過多解釋,想必大家都明白,我們還是來看程式碼吧。
由於屬於情懷特效(沒有具體的回撥事件要求),那麼就沒有必要自定義監聽,回撥處理,我直接把要處理的UI注入到自定義控制元件中,這樣她方便我也方便.
在此說明一下,前面部分實現中有誤,但是也希望您仔細品讀,相信您一定可以學到一些知識的。
首先我們將背景圖片和頂部線條注入到該控制元件中。接著我們看onTouchEvent事件,因為至始至終都是她在起作用.
- /***
- * 觸控事件
- *
- * @param ev
- */
- public void commOnTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- initTouchY = ev.getY();
- current_Top = initTop = imageView.getTop();
- current_Bottom = initBottom = imageView.getBottom();
- lineUp_current_Top = line_up_top = line_up.getTop();
- lineUp_current_Bottom = line_up_bottom = line_up.getBottom();
- break;
- case MotionEvent.ACTION_UP:
- /** 回縮動畫 **/
- if (isNeedAnimation()) {
- animation();
- }
- isMoveing = false;
- touchY = 0;// 手指鬆開要歸0.
- break;
- /***
- * 排除出第一次移動計算,因為第一次無法得知deltaY的高度, 然而我們也要進行初始化,就是第一次移動的時候讓滑動距離歸0.
- * 之後記錄準確了就正常執行.
- */
- case MotionEvent.ACTION_MOVE:
- Log.e(TAG, "isMoveing=" + isMoveing);
- touchY = ev.getY();
- float deltaY = touchY - initTouchY;// 滑動距離
- Log.e(TAG, "deltaY=" + deltaY);
- /** 過濾: **/
- if (deltaY < 0 && inner.getTop() <= 0) {
- return;
- }
- // 當滾動到最上或者最下時就不會再滾動,這時移動佈局
- isNeedMove();
- if (isMoveing) {
- // 初始化頭部矩形
- if (normal.isEmpty()) {
- // 儲存正常的佈局位置
- normal.set(inner.getLeft(), inner.getTop(),
- inner.getRight(), inner.getBottom());
- }
- // 移動佈局(手勢移動的1/3)
- float inner_move_H = deltaY / 5;
- inner.layout(normal.left, (int) (normal.top + inner_move_H),
- normal.right, (int) (normal.bottom + inner_move_H));
- /** image_bg **/
- float image_move_H = deltaY / 10;
- current_Top = (int) (initTop + image_move_H);
- current_Bottom = (int) (initBottom + image_move_H);
- imageView.layout(imageView.getLeft(), current_Top,
- imageView.getRight(), current_Bottom);
- /** line_up **/
- float line_up_H = inner_move_H;
- lineUp_current_Top = (int) (line_up_top + inner_move_H);
- lineUp_current_Bottom = (int) (line_up_bottom + inner_move_H);
- line_up.layout(line_up.getLeft(), lineUp_current_Top,
- line_up.getRight(), lineUp_current_Bottom);
- }
- break;
- default:
- break;
- }
- }
MotionEvent.ACTION_DOWN:觸控摁下獲取相應的座標.
MotionEvent.ACTION_MOVE:
裡面有個方法isNeedMove。作用:我們滑動的是ScrollView自身呢,還是我們自己模擬的那種滑動.
- /***
- * 是否需要移動佈局 inner.getMeasuredHeight():獲取的是控制元件的總高度
- *
- * getHeight():獲取的是螢幕的高度
- *
- * @return
- */
- public void isNeedMove() {
- int offset = inner.getMeasuredHeight() - getHeight();
- int scrollY = getScrollY();
- // 如果ScrollView的子View們沒有超過一螢幕則scrollY == 0,直接返回true,
- //如果ScrollView的子View們超過了一螢幕則 getScrollY()==offset說明滑到了ScrollView的低端.這時候才返回true.
- if (scrollY == 0 || scrollY == offset) {
- isMoveing = true;
- }
- }
MotionEvent.ACTION_UP:就是做些善後操作,主要看animation方法.
- /***
- * 回縮動畫
- */
- public void animation() {
- TranslateAnimation image_Anim = new TranslateAnimation(0, 0,
- Math.abs(initTop - current_Top), 0);
- image_Anim.setDuration(200);
- imageView.startAnimation(image_Anim);
- imageView.layout(imageView.getLeft(), (int) initTop,
- imageView.getRight(), (int) initBottom);
- // 開啟移動動畫
- TranslateAnimation inner_Anim = new TranslateAnimation(0, 0,
- inner.getTop(), normal.top);
- inner_Anim.setDuration(200);
- inner.startAnimation(inner_Anim);
- inner.layout(normal.left, normal.top, normal.right, normal.bottom);
- /** line_up **/
- TranslateAnimation line_up_Anim = new TranslateAnimation(0, 0,
- Math.abs(line_up_top - lineUp_current_Top), 0);
- line_up_Anim.setDuration(200);
- line_up.startAnimation(line_up_Anim);
- line_up.layout(line_up.getLeft(), line_up_top, line_up.getRight(),
- line_up_bottom);
- normal.setEmpty();
- /** 動畫執行 **/
- if (current_Top > initTop + 50 && turnListener != null)
- turnListener.onTurn();
- }
比如:我們的背景圖片原先座標為:(0,-190,800,300),隨著手勢移動到(0,-100,800,390)移動了90畫素,那麼我們的TranslateAnimation應該如何寫呢?我之前總認為不就是末尾座標指向初始座標不就完了,結果你會發現,動畫根本不起作用而是一閃而過。原因呢,動畫引數不可以為負數.或許因為動畫是以(0,0)為參照物吧.因此要把動畫寫成TranslateAnimation line_up_Anim = new TranslateAnimation(0, 0,Math.abs(-190- (-100)), 0);這樣我們所需要的動畫效果就實現了.
但是新的問題又出現了:
當你下拉到一定狀態後然後慢慢向上移動,會發現移動的很快(沒有回縮的反應),而移動到最頂部的時候突然又出現反彈效果。這個效果固然不是我們所需要的那種。我們所需要的效果是:下拉到一定程度,然後反過來上拉的時候要慢慢的移動回到原點(中心位置)停止。如果是上拉的話,不要出現反彈效果,如果是下拉鬆開的話,出現反彈效果。
描述的有點亂,如果想知道具體效果的話,我建議你使用下papa,其實國內這些比較優秀的應用UI都是抄襲國外的,如果你用facebook的話,就會發現,怎麼啪啪的個人頁面長的也忒像facebook了。請看下圖:
嘿嘿,不好意思,跑題了,針對上面出現的問題,我簡單說明一下.
首先,比如我們手勢下拉了50畫素,其實是使得自定義ScrollView的孩子也就是LinearLayout這個控制元件的top為50,而這個時候的getScrollY()的值仍為0,但是如果此時你停止下拉反而向上拉取的話,那麼此時的getScrollY()會從0開始逐漸增大,當我們移動到頂部也就是將ScrollView移動到最底部,此時的isMoveing為true,所以你繼續上拉的話會出現反彈效果。
這個問題要如何解決呢,其實也不難,但是我糾結了好長時間,也走了好多彎路。在這裡說明一下我的瞎跑路段以及疑問:當時我就想,getScrollY()這麼不聽話,我何必非要對ScrollView的孩子進行操作呢,為何直接不對本控制元件執行layout(l,t,r,b)呢,後來就照著這個邏輯進行update,終於更改了差不多了,糾結了問題再次出現,在你下拉的時候對ScrollView本身執行layout(l,t,r,b)這個方法可以實現反彈效果,但是此時你確無法進行滑動了,就是ScrollView本身的滑動無緣無故的被禁止掉了.我懷疑是layout的時候引數弄錯了。,後來仔細修改了下發現還是不可以滑動,然後google了半天也杳無音訊,最後固然放棄,又回到了原點。接著琢磨。。。算是功夫不負有心人吧,最終想到了解決方案,希望對您有幫助。
還拿上面說到的那短話,比如我們手勢下拉了50畫素,那麼此時touch的距離也就是50畫素,如果此時我們反向上拉的話,同樣是需要50畫素回到最初的位置。說到這裡我想大家都明白了。(首先我們要將操作分開,分為UP,DOWN,如果是DOWN的話,那麼在下拉後執行上拉的時候我們禁用掉自定義控制元件的滑動,而是通過手勢執行layout執行這50畫素.)
下面我們看部分程式碼:
- /**對於首次Touch操作要判斷方位:UP OR DOWN**/
- if (deltaY < 0 && state == state.NOMAL) {
- state = State.UP;
- } else if (deltaY > 0 && state == state.NOMAL) {
- state = State.DOWN;
- }
- if (state == State.UP) {
- deltaY = deltaY < 0 ? deltaY : 0;
- isMoveing = false;
- shutTouch = false;
- } else if (state == state.DOWN) {
- if (getScrollY() <= deltaY) {
- shutTouch = true;
- isMoveing = true;
- }
- deltaY = deltaY < 0 ? 0 : deltaY;
- }
程式碼很簡單,不過多解釋了,不明白的話,仔細看下原始碼肯定就明白了。
touch 事件處理:
- /** touch 事件處理 **/
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (inner != null) {
- commOnTouchEvent(ev);
- }
- // ture:禁止控制元件本身的滑動.
- if (shutTouch)
- return true;
- else
- return super.onTouchEvent(ev);
- }
說明:如果返回值為true,作用:禁止ScrollView的滑動,此時的Touch事件還存哦!!!如果對Touch事件比較熟悉的同學,相信覺得我有點廢話了,哈哈,我也是個小菜鳥,也卡在這裡過。
最後呢,還有個小BUG,也就是那個頂部拉線,如果你讓ScrollView慣性滑動的話,那麼你會發現,頂部線條沒有跟隨移動,其實就是因為慣性滑動的時候我們是獲取不到getScrollY()的值得造成的,查了半天也沒有找到相關資料,這個問題就暫時就留在這裡,有時間了在續。
這裡我將原始碼貼出來:
- package com.example.scrollviewdemo;
- import android.content.Context;
- import android.graphics.Rect;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.animation.TranslateAnimation;
- import android.widget.ImageView;
- import android.widget.ScrollView;
- /**
- * 自定義ScrollView
- *
- * @author jia
- *
- */
- public class PersonalScrollView extends ScrollView {
- private final String TAG = PersonalScrollView.class.getSimpleName();
- private View inner;// 孩子View
- private float touchY;// 點選時Y座標
- private float deltaY;// Y軸滑動的距離
- private float initTouchY;// 首次點選的Y座標
- private boolean shutTouch = false;// 是否關閉ScrollView的滑動.
- private Rect normal = new Rect();// 矩形(這裡只是個形式,只是用於判斷是否需要動畫.)
- private boolean isMoveing = false;// 是否開始移動.
- private ImageView imageView;// 背景圖控制元件.
- private View line_up;// 上線
- private int line_up_top;// 上線的top
- private int line_up_bottom;// 上線的bottom
- private int initTop, initBottom;// 初始高度
- private int current_Top, current_Bottom;// 拖動時時高度。
- private int lineUp_current_Top, lineUp_current_Bottom;// 上線
- private onTurnListener turnListener;
- private ImageView imageHeader;
- public void setImageHeader(ImageView imageHeader) {
- this.imageHeader = imageHeader;
- }
- // 狀態:上部,下部,預設
- private enum State {
- UP, DOWN, NOMAL
- };
- // 預設狀態
- private State state = State.NOMAL;
- public void setTurnListener(onTurnListener turnListener) {
- this.turnListener = turnListener;
- }
- public void setLine_up(View line_up) {
- this.line_up = line_up;
- }
- // 注入背景圖
- public void setImageView(ImageView imageView) {
- this.imageView = imageView;
- }
- /***
- * 構造方法
- *
- * @param context
- * @param attrs
- */
- public PersonalScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- /***
- * 根據 XML 生成檢視工作完成.該函式在生成檢視的最後呼叫,在所有子檢視新增完之後. 即使子類覆蓋了 onFinishInflate
- * 方法,也應該呼叫父類的方法,使該方法得以執行.
- */
- @Override
- protected void onFinishInflate() {
- if (getChildCount() > 0) {
- inner = getChildAt(0);
- }
- }
- /** touch 事件處理 **/
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (inner != null) {
- commOnTouchEvent(ev);
- }
- // ture:禁止控制元件本身的滑動.
- if (shutTouch)
- return true;
- else
- return super.onTouchEvent(ev);
- }
- /***
- * 觸控事件
- *
- * @param ev
- */
- public void commOnTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- initTouchY = ev.getY();
- current_Top = initTop = imageView.getTop();
- current_Bottom = initBottom = imageView.getBottom();
- if (line_up_top == 0) {
- lineUp_current_Top = line_up_top = line_up.getTop();
- lineUp_current_Bottom = line_up_bottom = line_up.getBottom();
- }
- break;
- case MotionEvent.ACTION_UP:
- /** 回縮動畫 **/
- if (isNeedAnimation()) {
- animation();
- }
- if (getScrollY() == 0) {
- state = State.NOMAL;
- }
- isMoveing = false;
- touchY = 0;
- shutTouch = false;
- break;
- /***
- * 排除出第一次移動計算,因為第一次無法得知deltaY的高度, 然而我們也要進行初始化,就是第一次移動的時候讓滑動距離歸0.
- * 之後記錄準確了就正常執行.
- */
- case MotionEvent.ACTION_MOVE:
- touchY = ev.getY();
- deltaY = touchY - initTouchY;// 滑動距離
- /** 對於首次Touch操作要判斷方位:UP OR DOWN **/
- if (deltaY < 0 && state == state.NOMAL) {
- state = State.UP;
- } else if (deltaY > 0 && state == state.NOMAL) {
- state = State.DOWN;
- }
- if (state == State.UP) {
- deltaY = deltaY < 0 ? deltaY : 0;
- isMoveing = false;
- shutTouch = false;
- /** line_up **/
- lineUp_current_Top = (int) (line_up_top - getScrollY());
- lineUp_current_Bottom = (int) (line_up_bottom - getScrollY());
- Log.e(TAG, "top=" + getScrollY());
- line_up.layout(line_up.getLeft(), lineUp_current_Top,
- line_up.getRight(), lineUp_current_Bottom);
- } else if (state == state.DOWN) {
- if (getScrollY() <= deltaY) {
- shutTouch = true;
- isMoveing = true;
- }
- deltaY = deltaY < 0 ? 0 : deltaY;
- }
- if (isMoveing) {
- // 初始化頭部矩形
- if (normal.isEmpty()) {
- // 儲存正常的佈局位置
- normal.set(inner.getLeft(), inner.getTop(),
- inner.getRight(), inner.getBottom());
- }
- // 移動佈局(手勢移動的1/3)
- float inner_move_H = deltaY / 5;
- inner.layout(normal.left, (int) (normal.top + inner_move_H),
- normal.right, (int) (normal.bottom + inner_move_H));
- /** image_bg **/
- float image_move_H = deltaY / 10;
- current_Top = (int) (initTop + image_move_H);
- current_Bottom = (int) (initBottom + image_move_H);
- imageView.layout(imageView.getLeft(), current_Top,
- imageView.getRight(), current_Bottom);
- /** line_up **/
- lineUp_current_Top = (int) (line_up_top + inner_move_H);
- lineUp_current_Bottom = (int) (line_up_bottom + inner_move_H);
- line_up.layout(line_up.getLeft(), lineUp_current_Top,
- line_up.getRight(), lineUp_current_Bottom);
- }
- break;
- default:
- break;
- }
- }
- /***
- * 回縮動畫
- */
- public void animation() {
- TranslateAnimation image_Anim = new TranslateAnimation(0, 0,
- Math.abs(initTop - current_Top), 0);
- image_Anim.setDuration(200);
- imageView.startAnimation(image_Anim);
- imageView.layout(imageView.getLeft(), (int) initTop,
- imageView.getRight(), (int) initBottom);
- // 開啟移動動畫
- TranslateAnimation inner_Anim = new TranslateAnimation(0, 0,
- inner.getTop(), normal.top);
- inner_Anim.setDuration(200);
- inner.startAnimation(inner_Anim);
- inner.layout(normal.left, normal.top, normal.right, normal.bottom);
- /** line_up **/
- TranslateAnimation line_up_Anim = new TranslateAnimation(0, 0,
- Math.abs(line_up_top - lineUp_current_Top), 0);
- line_up_Anim.setDuration(200);
- line_up.startAnimation(line_up_Anim);
- line_up.layout(line_up.getLeft(), line_up_top, line_up.getRight(),
- line_up_bottom);
- normal.setEmpty();
- /** 動畫執行 **/
- if (current_Top > initTop + 50 && turnListener != null)
- turnListener.onTurn();
- }
- /** 是否需要開啟動畫 **/
- public boolean isNeedAnimation() {
- return !normal.isEmpty();
- }
- /***
- * 執行翻轉
- *
- * @author jia
- *
- */
- public interface onTurnListener {
- /** 必須達到一定程度才執行 **/
- void onTurn();
- }
- }
介面有點醜陋,不過UI可以自己根據需求進行調整.
最後我在多侃一點,這裡我用的是TableLayout佈局,不是ListView,因為ListView和ScrollView本身就有衝突,雖說有解決方案,但是我還是喜歡用TableLayout。程式碼裡面模擬了3D旋轉效果,這裡就不解釋了,網上相關文章也有好多。就說到這裡,將原始碼供出,如果有問題請留言。
相關文章
- Android圖片壓縮實現過程及程式碼Android
- Android程式碼實現自定義ButtonAndroid
- CSS實現的背景圖片替代顏色程式碼CSS
- 自定義Drawable:實現文字生成圖片
- 自定義drawable實現圓角圖片
- 自定義view實現圓角圖片View
- canvas實現的圖片縮放程式碼例項Canvas
- 實現圖片縮放
- css實現滑鼠滑過切換背景圖片程式碼CSS
- 自定義圖片裁剪之雙指縮放思路
- 瀑布流程式碼實現及思路
- 實現背景圖片的全屏拉伸效果
- Highcharts 實現自定義匯出圖片
- android 自定義ImageView實現圖片手勢滑動,多點觸控放大縮小效果AndroidView
- JNI實現圖片壓縮
- 自定義View:畫布實現自定義View(折線圖的實現)View
- Android Toast 自定義背景、圖片 隨心使用AndroidAST
- android實現多圖片放大縮小的切換Android
- Android自定義拍照實現Android
- Android 實現自定義圓環Android
- Android自定義圓形進度條實現程式碼Android
- 利用 canvas 實現圖片壓縮Canvas
- Android自定義View之圖片外形特效——輕鬆實現圓角和圓形圖片AndroidView特效
- 圖片純前端JS壓縮的實現前端JS
- Android之AppBarLayout實現懸停吸附伸縮效果AndroidAPP
- 直播系統程式碼,Android自定義View實現呼吸燈效果AndroidView
- WIN10UI—實現思路分享及程式碼Win10UI
- css實現圖片背景填充的正六邊形CSS
- 直播平臺搭建,實現圖片縮圖功能
- IOS 自定義 UIDatePicker 背景圖片iOSUI
- [Android]程式碼實現ColorStateList及StateListDrawableAndroid
- EventSource的自定義實現
- layui中實現上傳圖片壓縮UI
- web前端實現圖片壓縮處理Web前端
- 滑鼠懸浮圖片實現縮放效果
- JS實現等比例縮放圖片JS
- 純前端實現 JPG 圖片壓縮 | canvas前端Canvas
- 微服務-分解應用程式從而實現更好的部署特性及可伸縮性微服務