不逼逼,看效果!兩邊有點露出來的效果,比如騰訊視頻App的上方的效果,都是輕量級的控件,請勿見怪,總體時間花費大約9個小時,其中找Bug找了3個小時,哈哈!
-
第一個效果是正常的滑動情況
xiao.gif -
第二個效果是禁止滑動情況,同時呢,有一個回彈的效果,四川話講這個很巴適
xiao.gif
分享兩個東西
- 今天發現的一個Android UI 開發效率的 UI 庫:https://github.com/QMUI/QMUI_Android
- 這個我都不好意思分享,嘿嘿,周天就做這個,做完了發現根本沒有什么東西可以分享,所以就寫了現在這個博客,等我以后研究下hexo,才來更新
- 2018.8.23 終于搭建完成了新的域名地址 www.shiming.site
寫在前面的話:如果我手寫慢一點,多看看一下api,我就不會把兩個api寫錯了,由于手滑寫錯了,導致我這篇博客現在才來寫,興奮感都快磨完了。
- 這輩子我都不會忘記這個值了,getScaledTouchSlop()是一個距離,表示滑動的時候,手的移動要大于這個距離才開始移動控件。viewpager就是用這個距離來判斷用戶是否翻頁,只不過呢
ViewConfiguration.get(mContext).getScaledTouchSlop()
原理如下: mTouchSlop = configuration.getScaledPagingTouchSlop();就是這個值,但是你可能會說有毛的的關系啊,別急
往ViewConfiguration類看記住這個值
看這個值mTouchSlop,對吧只不過在ViewPager判斷是否需要移動的時候,這個距離是*2。
由于我這里需要更高的精度,所以獲取了這個值getScaledTouchSlop()
- 可千萬不要拿到getScaledDoubleTapSlop()這個值了啊!
//第一次觸摸和第二次觸摸之間的距離,Distance in pixels between the first touch and second touch
ViewConfiguration.get(mContext).getScaledDoubleTapSlop();
繼承ViewGroup,重寫構造方法
public class CardViewPager extends ViewGroup{
public CardViewPager(Context context) {
this(context,null);
}
public CardViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
}
初始化init
private void init(Context context) {
mContext = context;
//滑動的對象
mScroller = new Scroller(mContext);
//getScaledTouchSlop是一個距離,表示滑動的時候,手的移動要大于這個距離才開始移動控件。
// 如果小于這個距離就不觸發移動控件,如viewpager就是用這個距離來判斷用戶是否翻頁
// mScaledDoubleTapSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
mScaledDoubleTapSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
//第一次觸摸和第二次觸摸之間的距離,Distance in pixels between the first touch and second touch
ViewConfiguration.get(mContext).getScaledDoubleTapSlop();
FIRST_width = dp2px(mContext, 10);
TWO_GAP_WIDTH = FIRST_width * 2;
THREE_GAP_WIDTH = FIRST_width * 3;
FOUR_GAP_WIDTH = FIRST_width * 4;
}
onMeasure,重寫測量這里記住widthMeasureSpec、heightMeasureSpec是一個32位的int值,其中高兩位是物理模式,低的30位才是控件的寬度和高度的信息。
MeasureSpec.EXACTLY:父視圖希望子視圖的大小應該是specSize中指定的。
MeasureSpec.AT_MOST:子視圖的大小最多是specSize中指定的值,也就是說不建議子視圖的大小超過specSize中給定的值。
MeasureSpec.UNSPECIFIED:我們可以隨意指定視圖的大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//這才是真正的寬度和高度
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heighSize = MeasureSpec.getSize(heightMeasureSpec);
//設置測量的大小
setMeasuredDimension(widthSize,heighSize);
//測量孩子的大小
mChildCount = getChildCount();
for (int i=0;i<mChildCount;i++){
//這里需要把模式也傳入進去
getChildAt(i).measure(widthMeasureSpec,heightMeasureSpec);
}
}
onLayout重新布局:將孩子的view布局,這里橫向的布局,一個字View接著右邊,這是設計之初的方法,自己先明白到底是怎么樣布局,就好像我明白的方式,是個偉大的ui妹子說,看這個app,就是這樣,哈哈
/**
* @param changed
* @param l 左上角的left
* @param t top
* @param r 右下角right
* @param b bottom值
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View child;
int widthLeft=0;
for (int i=0;i<mChildCount;i++){
child = getChildAt(i);
//得到第一個孩子的寬度,兩邊都減去了兩個參數,記住是這個4倍值
int measuredWidth = child.getMeasuredWidth() - FOUR_GAP_WIDTH;
int measuredHeight = child.getMeasuredHeight();
//是第一個孩子
if (i==0){
child.layout(widthLeft+TWO_GAP_WIDTH,0,widthLeft+TWO_GAP_WIDTH+measuredWidth,measuredHeight);
//改變向左的值
widthLeft+=measuredWidth+THREE_GAP_WIDTH;
}else {
child.layout(widthLeft, 0, widthLeft + measuredWidth, measuredHeight);
widthLeft += measuredWidth + FIRST_width;
}
}
}
效果雖然看了,但是真正理解的layout的話,還需明白其中的原理,這里我不講了的太細,獻上美圖一張,嗦嘎,原理就是,不是每一個屏幕都在裝著一個我們的卡片,我們每次移動的時候,也不是移動一個屏幕,而是通過運算的方式,移動到恰好能夠看到兩邊10dp的值,這里的要轉成像素
dp2px(mContext, 10);
關于像素px我還想說說: context.getResources().getDisplayMetrics().density;density顯示器的邏輯密度,這是【獨立的像素密度單位(首先明白dp是個單位)】的一個縮放因子,在屏幕密度大約為160dpi的屏幕上,一個dp等于一個px,這個提供了系統顯示器的一個基線. 例如:屏幕為240320的手機屏幕,其尺寸為 1.5"2" 也就是1.5英寸乘2英寸的屏幕 它的dpi(屏幕像素密度,也就是每英寸的像素數,dpi是dot per inch的縮寫)大約就為160dpi, 所以在這個手機上dp和px的長度(可以說是長度,最起碼從你的視覺感官上來說是這樣的)是相等的。 因此在一個屏幕密度為160dpi的手機屏幕上density的值為1,而在120dpi的手機上為0.75等等.例如:一個240320的屏幕盡管他的屏幕尺寸為1.8"1.3",(我算了下這個的dpi大約為180dpi多點)但是它的density還是1(也就是說取了近似值) 然而,如果屏幕分辨率增加到320480 但是屏幕尺寸仍然保持1.5"2" 的時候(和最開始的例子比較)
這個手機的density將會增加(可能會增加到1.5)
public static int dp2px(Context context, float dpValue) {
///這個得到的不應該叫做密度,應該是密度的一個比例。不是真實的屏幕密度,
/// 而是相對于某個值的屏幕密度。也可以說是相對密度
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
攔截事件:當大于了需要移動控件的距離的話,就需要把這個事件攔截自己處理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
int x = (int) ev.getX();
mLastMotionX = x ;
break;
case MotionEvent.ACTION_MOVE:
x= (int) ev.getX();
//滑動的距離
int delX = mLastMotionX - x;
//如果說距離大于這個距離的話,就需要滾動了,攔截事件
if (Math.abs(delX)>mScaledDoubleTapSlop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
處理事件:在down的事件一定需要攔截,才能記錄坐標
返回值為True,代表攔截這次事件,直接進入到ViewGroup的onTouchEvent中,就不會進入到View的onTouchEvent了
返回值為False,代表不攔截這次事件,不進入到ViewGroup的onTouchEvent中,直接進入到View的onTouchEvent中
public boolean onTouchEvent(MotionEvent event) {
//如果沒有孩子的話,不需要攔截
if (getChildCount()==0){
return false;
}
//監聽滑動的速度
obtainTracker(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
//停止動畫
mScroller.abortAnimation();
}
int x = (int) event.getX();
mLastMotionX=x;
//不管怎么怎么樣這個事件都必須攔截
return true;
case MotionEvent.ACTION_MOVE:
x = (int) event.getX();
int desX = mLastMotionX - x;
//這個距離大于了屏幕的10/1的話,就給他賦值10/1
if (!isAllowScroll&&desX>getWidth()/10){
desX=getWidth()/10;
//如果設置了不可以滑動的,這個flag需要到up事件單獨處理
mCanScrolled = true;
}
//只需計算x的距離
mVelocityTracker.computeCurrentVelocity(1000,ViewConfiguration.getMaximumFlingVelocity());
mXVelocity = (int) mVelocityTracker.getXVelocity();
//如果說距離滑動太小,或者是只有一個屏幕的話,就不往下去做操作了
if (Math.abs(desX)<mScaledDoubleTapSlop||(desX>=0&&
mCurScreen==mChildCount-1)||(desX<=0&&mCurScreen==0)){
break;
}
//能到這里來的話,就必須往手指方向慢慢滾動了
scrollTo(getChildAt(mCurScreen).getLeft()+desX,0);
break;
case MotionEvent.ACTION_UP:
//mXVelocity為正數的話,這個是往left滾
if (isAllowScroll&&mXVelocity>MAX_VELOCITY_VALUE&&mCurScreen>0){
scrollScreen(mCurScreen-1);
}else if (isAllowScroll&&mXVelocity<-MAX_VELOCITY_VALUE&&mCurScreen<mChildCount-1){
scrollScreen(mCurScreen+1);
//當設置了不能滑動時候,并且手指滑動的Move的距離已經超過了屏幕的10/1,有一個回彈的效果,左右搖擺
}else if (mCanScrolled){
springToDestination();
}else{
snapToDestination();
}
//最后不要忘記了釋放
releaseVelocityTracker();
break;
}
return super.onTouchEvent(event);
}
滑動到指定的屏幕,在記住兩個地方,就不需要滑動了,一個是在最右和最左端。
private void scrollScreen(int whichScreen) {
//防止超出了最大的孩子的數量
int min = Math.min(whichScreen, mChildCount - 1);
whichScreen = Math.max(0, min);
//getScrollX() 就是當前view的左上角相對于母視圖的左上角的X軸偏移量。
//在這里當getScrollX==0的時候,等于后面的whichScreen*getWidth()那么就滑動到第一頁了
//后續就不需要滑動了,也不需要重新繪制了
// TODO: 2017/9/3 這里只在最左不能進去滑動了,其實在最右端也是不能夠去滑動了,帶解決
if (getScrollX()!=whichScreen*getWidth()){
int deltaX = whichScreen * (getWidth() - THREE_GAP_WIDTH) - getScrollX();
mCurScreen = whichScreen;
mScroller.startScroll(getScrollX(), 0, deltaX, 0, Math.abs(deltaX));
invalidate();
}
}
但是在這里我留下一個問題,但是在這里我留下一個問題在我的手機上我測試了1到5個孩子的情況分別數據如下:
getScrollX()和whichScreen*getWidth()
1個屏幕是0 0--------0
2個屏幕是90 990 -------1080
3個屏是 120
4個屏 =270 2970 -------3240
5個屏 =380 3960 -------4320
當我們滑動到最有端的時候,其實也是不能夠去滑動了,但是我這個方法呢是能夠 走到if當中的,而且對應關系也不太明確,這個問題我還得想想。
還需要理解一個東西getScrollX()到底是什么值?再次獻上我的美作,哈哈,反正我是明白了,我怕講不明白,所以先看圖,然后抓日記,一下就明白!
監聽滑動的速度,在上篇筆鋒效果里面有講到過,還是Viewpager里面的東西,記住要釋放這個算是監聽吧!
//監聽滑動的速度
obtainTracker(event);
private void obtainTracker(MotionEvent event) {
if (mVelocityTracker==null) {
mVelocityTracker = VelocityTracker.obtain();
}
//綁定事件
mVelocityTracker.addMovement(event);
}
/**
* 釋放監聽滑動速度方法
*/
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
其實你就把上面的工作全部都做好了,你會發現還是不能夠翻頁,來吧去Viewpager看看,再去度娘看看
/**
* 計算滾動的位置
*/
@Override
public void computeScroll() {
super.computeScroll();
//返回值為boolean,mScroller.computeScrollOffset()==true說明滾動尚未完成,false說明滾動已經完成。
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
個人翻譯就是在viewpager中需要重新計算滑動的位置
回彈滑動目的屏,這里就是需要有點動畫的效果,先回到原來的位置,然后左右搖擺搖擺!
/**
* 回彈滑動目的屏
*/
private void springToDestination() {
System.out.println("shiming ==springToDestination");
int screenWidth = getWidth();
int whichScreen = (getScrollX() + screenWidth / 2) / screenWidth;
whichScreen = Math.max(0, Math.min(whichScreen, mChildCount - 1));
final int deltaX = whichScreen * (getWidth() - THREE_GAP_WIDTH) - getScrollX();
mCurScreen = whichScreen;
//先給我滾動到原來的位置
springToScroll(deltaX * 1.0f, Math.abs(deltaX));
//向右的給我擺動兩下
postDelayed(new Runnable() {
@Override
public void run() {
springToScroll(-deltaX * 0.3f, Math.abs(deltaX));
}
}, Math.abs(deltaX));
//讓后給我向左擺動兩下
postDelayed(new Runnable() {
@Override
public void run() {
springToScroll(deltaX * 0.3f, Math.abs(deltaX));
}
}, Math.abs(deltaX * 2));
mCanScrolled = false;
}
/**
* getScrollX() 水平方向滾動的偏移值,以像素為單位。正值表明滾動將向左滾動
startY 垂直方向滾動的偏移值,以像素為單位。正值表明滾動將向上滾動
(int) deltaX 水平方向滑動的距離,正值會使滾動向左滾動
0 垂直方向滑動的距離,正值會使滾動向上滾動
* @param deltaX
* @param duration
*/
private void springToScroll(float deltaX, int duration) {
mScroller.startScroll(getScrollX(), 0, (int) deltaX, 0, duration);
invalidate();
}