不逼逼,看效果星澳!兩邊有點(diǎn)露出來(lái)的效果疚顷,比如騰訊視頻App的上方的效果,都是輕量級(jí)的控件禁偎,請(qǐng)勿見(jiàn)怪腿堤,總體時(shí)間花費(fèi)大約9個(gè)小時(shí),其中找Bug找了3個(gè)小時(shí)届垫,哈哈释液!
-
第一個(gè)效果是正常的滑動(dòng)情況
xiao.gif -
第二個(gè)效果是禁止滑動(dòng)情況全释,同時(shí)呢装处,有一個(gè)回彈的效果,四川話講這個(gè)很巴適
xiao.gif
分享兩個(gè)東西
- 今天發(fā)現(xiàn)的一個(gè)Android UI 開(kāi)發(fā)效率的 UI 庫(kù):https://github.com/QMUI/QMUI_Android
- 這個(gè)我都不好意思分享,嘿嘿妄迁,周天就做這個(gè)寝蹈,做完了發(fā)現(xiàn)根本沒(méi)有什么東西可以分享,所以就寫了現(xiàn)在這個(gè)博客登淘,等我以后研究下hexo箫老,才來(lái)更新
- 2018.8.23 終于搭建完成了新的域名地址 www.shiming.site
寫在前面的話:如果我手寫慢一點(diǎn),多看看一下api黔州,我就不會(huì)把兩個(gè)api寫錯(cuò)了耍鬓,由于手滑寫錯(cuò)了,導(dǎo)致我這篇博客現(xiàn)在才來(lái)寫流妻,興奮感都快磨完了牲蜀。
- 這輩子我都不會(huì)忘記這個(gè)值了,getScaledTouchSlop()是一個(gè)距離绅这,表示滑動(dòng)的時(shí)候涣达,手的移動(dòng)要大于這個(gè)距離才開(kāi)始移動(dòng)控件。viewpager就是用這個(gè)距離來(lái)判斷用戶是否翻頁(yè)证薇,只不過(guò)呢
ViewConfiguration.get(mContext).getScaledTouchSlop()
原理如下: mTouchSlop = configuration.getScaledPagingTouchSlop();就是這個(gè)值度苔,但是你可能會(huì)說(shuō)有毛的的關(guān)系啊,別急
往ViewConfiguration類看記住這個(gè)值
看這個(gè)值mTouchSlop浑度,對(duì)吧只不過(guò)在ViewPager判斷是否需要移動(dòng)的時(shí)候寇窑,這個(gè)距離是*2。
由于我這里需要更高的精度俺泣,所以獲取了這個(gè)值getScaledTouchSlop()
- 可千萬(wàn)不要拿到getScaledDoubleTapSlop()這個(gè)值了傲迫稀!
//第一次觸摸和第二次觸摸之間的距離,Distance in pixels between the first touch and second touch
ViewConfiguration.get(mContext).getScaledDoubleTapSlop();
繼承ViewGroup,重寫構(gòu)造方法
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;
//滑動(dòng)的對(duì)象
mScroller = new Scroller(mContext);
//getScaledTouchSlop是一個(gè)距離伏钠,表示滑動(dòng)的時(shí)候横漏,手的移動(dòng)要大于這個(gè)距離才開(kāi)始移動(dòng)控件。
// 如果小于這個(gè)距離就不觸發(fā)移動(dòng)控件熟掂,如viewpager就是用這個(gè)距離來(lái)判斷用戶是否翻頁(yè)
// 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,重寫測(cè)量這里記住widthMeasureSpec缎浇、heightMeasureSpec是一個(gè)32位的int值,其中高兩位是物理模式赴肚,低的30位才是控件的寬度和高度的信息素跺。
MeasureSpec.EXACTLY:父視圖希望子視圖的大小應(yīng)該是specSize中指定的。
MeasureSpec.AT_MOST:子視圖的大小最多是specSize中指定的值誉券,也就是說(shuō)不建議子視圖的大小超過(guò)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);
//設(shè)置測(cè)量的大小
setMeasuredDimension(widthSize,heighSize);
//測(cè)量孩子的大小
mChildCount = getChildCount();
for (int i=0;i<mChildCount;i++){
//這里需要把模式也傳入進(jìn)去
getChildAt(i).measure(widthMeasureSpec,heightMeasureSpec);
}
}
onLayout重新布局:將孩子的view布局踊跟,這里橫向的布局踩验,一個(gè)字View接著右邊,這是設(shè)計(jì)之初的方法,自己先明白到底是怎么樣布局,就好像我明白的方式箕憾,是個(gè)偉大的ui妹子說(shuō)牡借,看這個(gè)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);
//得到第一個(gè)孩子的寬度钠龙,兩邊都減去了兩個(gè)參數(shù),記住是這個(gè)4倍值
int measuredWidth = child.getMeasuredWidth() - FOUR_GAP_WIDTH;
int measuredHeight = child.getMeasuredHeight();
//是第一個(gè)孩子
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的話碴里,還需明白其中的原理,這里我不講了的太細(xì)上真,獻(xiàn)上美圖一張并闲,嗦嘎,原理就是谷羞,不是每一個(gè)屏幕都在裝著一個(gè)我們的卡片帝火,我們每次移動(dòng)的時(shí)候,也不是移動(dòng)一個(gè)屏幕湃缎,而是通過(guò)運(yùn)算的方式犀填,移動(dòng)到恰好能夠看到兩邊10dp的值,這里的要轉(zhuǎn)成像素
dp2px(mContext, 10);
關(guān)于像素px我還想說(shuō)說(shuō): context.getResources().getDisplayMetrics().density;density顯示器的邏輯密度嗓违,這是【獨(dú)立的像素密度單位(首先明白dp是個(gè)單位)】的一個(gè)縮放因子九巡,在屏幕密度大約為160dpi的屏幕上,一個(gè)dp等于一個(gè)px,這個(gè)提供了系統(tǒng)顯示器的一個(gè)基線. 例如:屏幕為240320的手機(jī)屏幕蹂季,其尺寸為 1.5"2" 也就是1.5英寸乘2英寸的屏幕 它的dpi(屏幕像素密度冕广,也就是每英寸的像素?cái)?shù),dpi是dot per inch的縮寫)大約就為160dpi偿洁, 所以在這個(gè)手機(jī)上dp和px的長(zhǎng)度(可以說(shuō)是長(zhǎng)度撒汉,最起碼從你的視覺(jué)感官上來(lái)說(shuō)是這樣的)是相等的。 因此在一個(gè)屏幕密度為160dpi的手機(jī)屏幕上density的值為1涕滋,而在120dpi的手機(jī)上為0.75等等.例如:一個(gè)240320的屏幕盡管他的屏幕尺寸為1.8"1.3",(我算了下這個(gè)的dpi大約為180dpi多點(diǎn))但是它的density還是1(也就是說(shuō)取了近似值) 然而睬辐,如果屏幕分辨率增加到320480 但是屏幕尺寸仍然保持1.5"2" 的時(shí)候(和最開(kāi)始的例子比較)
這個(gè)手機(jī)的density將會(huì)增加(可能會(huì)增加到1.5)
public static int dp2px(Context context, float dpValue) {
///這個(gè)得到的不應(yīng)該叫做密度,應(yīng)該是密度的一個(gè)比例宾肺。不是真實(shí)的屏幕密度溯饵,
/// 而是相對(duì)于某個(gè)值的屏幕密度。也可以說(shuō)是相對(duì)密度
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
攔截事件:當(dāng)大于了需要移動(dòng)控件的距離的話锨用,就需要把這個(gè)事件攔截自己處理丰刊。
@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();
//滑動(dòng)的距離
int delX = mLastMotionX - x;
//如果說(shuō)距離大于這個(gè)距離的話,就需要滾動(dòng)了增拥,攔截事件
if (Math.abs(delX)>mScaledDoubleTapSlop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
處理事件:在down的事件一定需要攔截啄巧,才能記錄坐標(biāo)
返回值為True洪橘,代表攔截這次事件,直接進(jìn)入到ViewGroup的onTouchEvent中棵帽,就不會(huì)進(jìn)入到View的onTouchEvent了
返回值為False,代表不攔截這次事件渣玲,不進(jìn)入到ViewGroup的onTouchEvent中逗概,直接進(jìn)入到View的onTouchEvent中
public boolean onTouchEvent(MotionEvent event) {
//如果沒(méi)有孩子的話,不需要攔截
if (getChildCount()==0){
return false;
}
//監(jiān)聽(tīng)滑動(dòng)的速度
obtainTracker(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
//停止動(dòng)畫
mScroller.abortAnimation();
}
int x = (int) event.getX();
mLastMotionX=x;
//不管怎么怎么樣這個(gè)事件都必須攔截
return true;
case MotionEvent.ACTION_MOVE:
x = (int) event.getX();
int desX = mLastMotionX - x;
//這個(gè)距離大于了屏幕的10/1的話忘衍,就給他賦值10/1
if (!isAllowScroll&&desX>getWidth()/10){
desX=getWidth()/10;
//如果設(shè)置了不可以滑動(dòng)的逾苫,這個(gè)flag需要到up事件單獨(dú)處理
mCanScrolled = true;
}
//只需計(jì)算x的距離
mVelocityTracker.computeCurrentVelocity(1000,ViewConfiguration.getMaximumFlingVelocity());
mXVelocity = (int) mVelocityTracker.getXVelocity();
//如果說(shuō)距離滑動(dòng)太小,或者是只有一個(gè)屏幕的話枚钓,就不往下去做操作了
if (Math.abs(desX)<mScaledDoubleTapSlop||(desX>=0&&
mCurScreen==mChildCount-1)||(desX<=0&&mCurScreen==0)){
break;
}
//能到這里來(lái)的話铅搓,就必須往手指方向慢慢滾動(dòng)了
scrollTo(getChildAt(mCurScreen).getLeft()+desX,0);
break;
case MotionEvent.ACTION_UP:
//mXVelocity為正數(shù)的話,這個(gè)是往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);
//當(dāng)設(shè)置了不能滑動(dòng)時(shí)候搀捷,并且手指滑動(dòng)的Move的距離已經(jīng)超過(guò)了屏幕的10/1星掰,有一個(gè)回彈的效果,左右搖擺
}else if (mCanScrolled){
springToDestination();
}else{
snapToDestination();
}
//最后不要忘記了釋放
releaseVelocityTracker();
break;
}
return super.onTouchEvent(event);
}
滑動(dòng)到指定的屏幕,在記住兩個(gè)地方嫩舟,就不需要滑動(dòng)了氢烘,一個(gè)是在最右和最左端。
private void scrollScreen(int whichScreen) {
//防止超出了最大的孩子的數(shù)量
int min = Math.min(whichScreen, mChildCount - 1);
whichScreen = Math.max(0, min);
//getScrollX() 就是當(dāng)前view的左上角相對(duì)于母視圖的左上角的X軸偏移量家厌。
//在這里當(dāng)getScrollX==0的時(shí)候播玖,等于后面的whichScreen*getWidth()那么就滑動(dòng)到第一頁(yè)了
//后續(xù)就不需要滑動(dòng)了,也不需要重新繪制了
// TODO: 2017/9/3 這里只在最左不能進(jìn)去滑動(dòng)了饭于,其實(shí)在最右端也是不能夠去滑動(dòng)了蜀踏,帶解決
if (getScrollX()!=whichScreen*getWidth()){
int deltaX = whichScreen * (getWidth() - THREE_GAP_WIDTH) - getScrollX();
mCurScreen = whichScreen;
mScroller.startScroll(getScrollX(), 0, deltaX, 0, Math.abs(deltaX));
invalidate();
}
}
但是在這里我留下一個(gè)問(wèn)題,但是在這里我留下一個(gè)問(wèn)題在我的手機(jī)上我測(cè)試了1到5個(gè)孩子的情況分別數(shù)據(jù)如下:
getScrollX()和whichScreen*getWidth()
1個(gè)屏幕是0 0--------0
2個(gè)屏幕是90 990 -------1080
3個(gè)屏是 120
4個(gè)屏 =270 2970 -------3240
5個(gè)屏 =380 3960 -------4320
當(dāng)我們滑動(dòng)到最有端的時(shí)候掰吕,其實(shí)也是不能夠去滑動(dòng)了果覆,但是我這個(gè)方法呢是能夠 走到if當(dāng)中的,而且對(duì)應(yīng)關(guān)系也不太明確殖熟,這個(gè)問(wèn)題我還得想想随静。
還需要理解一個(gè)東西getScrollX()到底是什么值?再次獻(xiàn)上我的美作吗讶,哈哈燎猛,反正我是明白了,我怕講不明白照皆,所以先看圖重绷,然后抓日記,一下就明白!
監(jiān)聽(tīng)滑動(dòng)的速度膜毁,在上篇筆鋒效果里面有講到過(guò)昭卓,還是Viewpager里面的東西,記住要釋放這個(gè)算是監(jiān)聽(tīng)吧愤钾!
//監(jiān)聽(tīng)滑動(dòng)的速度
obtainTracker(event);
private void obtainTracker(MotionEvent event) {
if (mVelocityTracker==null) {
mVelocityTracker = VelocityTracker.obtain();
}
//綁定事件
mVelocityTracker.addMovement(event);
}
/**
* 釋放監(jiān)聽(tīng)滑動(dòng)速度方法
*/
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
其實(shí)你就把上面的工作全部都做好了,你會(huì)發(fā)現(xiàn)還是不能夠翻頁(yè)候醒,來(lái)吧去Viewpager看看能颁,再去度娘看看
/**
* 計(jì)算滾動(dòng)的位置
*/
@Override
public void computeScroll() {
super.computeScroll();
//返回值為boolean,mScroller.computeScrollOffset()==true說(shuō)明滾動(dòng)尚未完成倒淫,false說(shuō)明滾動(dòng)已經(jīng)完成伙菊。
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
個(gè)人翻譯就是在viewpager中需要重新計(jì)算滑動(dòng)的位置
回彈滑動(dòng)目的屏,這里就是需要有點(diǎn)動(dòng)畫的效果,先回到原來(lái)的位置敌土,然后左右搖擺搖擺镜硕!
/**
* 回彈滑動(dòng)目的屏
*/
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;
//先給我滾動(dòng)到原來(lái)的位置
springToScroll(deltaX * 1.0f, Math.abs(deltaX));
//向右的給我擺動(dòng)兩下
postDelayed(new Runnable() {
@Override
public void run() {
springToScroll(-deltaX * 0.3f, Math.abs(deltaX));
}
}, Math.abs(deltaX));
//讓后給我向左擺動(dòng)兩下
postDelayed(new Runnable() {
@Override
public void run() {
springToScroll(deltaX * 0.3f, Math.abs(deltaX));
}
}, Math.abs(deltaX * 2));
mCanScrolled = false;
}
/**
* getScrollX() 水平方向滾動(dòng)的偏移值,以像素為單位返干。正值表明滾動(dòng)將向左滾動(dòng)
startY 垂直方向滾動(dòng)的偏移值兴枯,以像素為單位。正值表明滾動(dòng)將向上滾動(dòng)
(int) deltaX 水平方向滑動(dòng)的距離矩欠,正值會(huì)使?jié)L動(dòng)向左滾動(dòng)
0 垂直方向滑動(dòng)的距離财剖,正值會(huì)使?jié)L動(dòng)向上滾動(dòng)
* @param deltaX
* @param duration
*/
private void springToScroll(float deltaX, int duration) {
mScroller.startScroll(getScrollX(), 0, (int) deltaX, 0, duration);
invalidate();
}