Android自定义Toast提示框的显示效果实例

 更新时间:2016年9月20日 19:53  点击:2168
Android中的Toast是一种简易的消息提示框。Toast是一个包含用户点击消息。Toast类会帮助你创建和显示这些。本文我们来聊聊如何自定义Toast。

先看看效果图


Activity:

package com.example.editortoast;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.bt).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                toastShow();
            }
        });
    }
    private void toastShow() {
        LayoutInflater inflater = LayoutInflater.from(getApplicationContext());
        View view = inflater.inflate(R.layout.item_toast, null);
        TextView textView1 = (TextView) view.findViewById(R.id.TextView_1);
        textView1.setText("Toast1");
        Toast toast = new Toast(getApplicationContext());
        toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
        toast.setDuration(0);
        toast.setView(view);
        toast.show();
    }
}


activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.editortoast.MainActivity" >
    <Button
        android:id="@+id/bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击" />


</RelativeLayout>


item_toast.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <ImageView
        android:id="@+id/image"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerVertical="true"
        android:src="@drawable/ic_launcher" />
    <TextView
        android:id="@+id/TextView_1"
        android:textSize="30sp"
        android:textColor="@android:color/holo_red_light"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/image" />
</RelativeLayout>



android 自定义Toast,可设定显示时间


开发android的同学可能会抱怨Toast设定显示的时长无效,只能是Toast.LENGTH_LONG 或者Toast.LENGTH_SHORT 之一,为了解决这些办法,有多种实现方式:

1.使用定时器,定时调用show()方法.

2.使用CountDownTimer类,也是调用show()方法.

3.使用WindownManager类实现.

本文使用方法三进行实现,难度不大,直接看代码吧.

package com.open.toast;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
 * 自定义时长的Toast
 * @author DexYang
 *
 */
public class CToast {
  
  public static CToast makeText(Context context, CharSequence text, int duration) 
  {
    CToast result = new CToast(context);
    
    LinearLayout mLayout=new LinearLayout(context);
    TextView tv = new TextView(context);
    tv.setText(text);
    tv.setTextColor(Color.WHITE);
    tv.setGravity(Gravity.CENTER);
    mLayout.setBackgroundResource(R.drawable.widget_toast_bg);
    
    int w=context.getResources().getDisplayMetrics().widthPixels / 2;
    int h=context.getResources().getDisplayMetrics().widthPixels / 10;
    mLayout.addView(tv, w, h);
    result.mNextView = mLayout;
    result.mDuration = duration;
    return result;
  }
  
  public static final int LENGTH_SHORT = 2000;
  public static final int LENGTH_LONG = 3500;
  
  private final Handler mHandler = new Handler();    
  private int mDuration=LENGTH_SHORT;
  private int mGravity = Gravity.CENTER;
  private int mX, mY;
  private float mHorizontalMargin;
  private float mVerticalMargin;
  private View mView;
  private View mNextView;
  
  private WindowManager mWM;
  private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
  
  
   public CToast(Context context) {
      init(context);
    }
   
  /**
   * Set the view to show.
   * @see #getView
   */
  public void setView(View view) {
    mNextView = view;
  }
  /**
   * Return the view.
   * @see #setView
   */
  public View getView() {
    return mNextView;
  }
  /**
   * Set how long to show the view for.
   * @see #LENGTH_SHORT
   * @see #LENGTH_LONG
   */
  public void setDuration(int duration) {
    mDuration = duration;
  }
  /**
   * Return the duration.
   * @see #setDuration
   */
  public int getDuration() {
    return mDuration;
  }
  
  /**
   * Set the margins of the view.
   *
   * @param horizontalMargin The horizontal margin, in percentage of the
   *        container width, between the container's edges and the
   *        notification
   * @param verticalMargin The vertical margin, in percentage of the
   *        container height, between the container's edges and the
   *        notification
   */
  public void setMargin(float horizontalMargin, float verticalMargin) {
    mHorizontalMargin = horizontalMargin;
    mVerticalMargin = verticalMargin;
  }
  /**
   * Return the horizontal margin.
   */
  public float getHorizontalMargin() {
    return mHorizontalMargin;
  }
  /**
   * Return the vertical margin.
   */
  public float getVerticalMargin() {
    return mVerticalMargin;
  }
  /**
   * Set the location at which the notification should appear on the screen.
   * @see android.view.Gravity
   * @see #getGravity
   */
  public void setGravity(int gravity, int xOffset, int yOffset) {
    mGravity = gravity;
    mX = xOffset;
    mY = yOffset;
  }
   /**
   * Get the location at which the notification should appear on the screen.
   * @see android.view.Gravity
   * @see #getGravity
   */
  public int getGravity() {
    return mGravity;
  }
  /**
   * Return the X offset in pixels to apply to the gravity's location.
   */
  public int getXOffset() {
    return mX;
  }
  
  /**
   * Return the Y offset in pixels to apply to the gravity's location.
   */
  public int getYOffset() {
    return mY;
  }
  
  /**
   * schedule handleShow into the right thread
   */
  public void show() {
    mHandler.post(mShow);
    
    if(mDuration>0)
    {
       mHandler.postDelayed(mHide, mDuration);
    }
  }
  /**
   * schedule handleHide into the right thread
   */
  public void hide() {
    mHandler.post(mHide);
  }
  
  private final Runnable mShow = new Runnable() {
    public void run() {
      handleShow();
    }
  };
  private final Runnable mHide = new Runnable() {
    public void run() {
      handleHide();
    }
  };
  private void init(Context context)
  {    
     final WindowManager.LayoutParams params = mParams;
     params.height = WindowManager.LayoutParams.WRAP_CONTENT;
     params.width = WindowManager.LayoutParams.WRAP_CONTENT;
     params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
         | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
         | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
     params.format = PixelFormat.TRANSLUCENT;
     params.windowAnimations = android.R.style.Animation_Toast;
     params.type = WindowManager.LayoutParams.TYPE_TOAST;
     params.setTitle("Toast");
     
     mWM = (WindowManager) context.getApplicationContext()
         .getSystemService(Context.WINDOW_SERVICE);
  }
  
  
  private void handleShow() {
    if (mView != mNextView) {
      // remove the old view if necessary
      handleHide();
      mView = mNextView;
//            mWM = WindowManagerImpl.getDefault();
      final int gravity = mGravity;
      mParams.gravity = gravity;
      if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) 
      {
        mParams.horizontalWeight = 1.0f;
      }
      if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) 
      {
        mParams.verticalWeight = 1.0f;
      }
      mParams.x = mX;
      mParams.y = mY;
      mParams.verticalMargin = mVerticalMargin;
      mParams.horizontalMargin = mHorizontalMargin;
      if (mView.getParent() != null) 
      {
        mWM.removeView(mView);
      }
      mWM.addView(mView, mParams);
    }
  }
  private void handleHide() 
  {
    if (mView != null) 
    {
      if (mView.getParent() != null) 
      {
        mWM.removeView(mView);
      }
      mView = null;
    }
  }
}


测试类的代码如下:

package com.open.toast;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends Activity {
  
  private EditText mEditText;
  private CToast mCToast;
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    init();
  }
  
  private void init()
  {
    mEditText=(EditText)findViewById(R.id.timeEditText);
    findViewById(R.id.showToastBtn).setOnClickListener(listener);
    findViewById(R.id.hideToastBtn).setOnClickListener(listener);
  }
  
  private View.OnClickListener listener=new View.OnClickListener() {
    
    @Override
    public void onClick(View v) {
      switch(v.getId())
      {
        case R.id.showToastBtn:
          if(null!=mCToast)
          {
            mCToast.hide();
          }
          int time=TextUtils.isEmpty(mEditText.getText().toString())?CToast.LENGTH_SHORT:Integer.valueOf(mEditText.getText().toString());
          mCToast=CToast.makeText(getApplicationContext(), "我来自CToast!",time);
          mCToast.show();
          break;
      
        case R.id.hideToastBtn:
          if(null!=mCToast)
          {
            mCToast.hide();
          }
          break;
      }
      
    }
  };
  
}


效果如下: 


本文我们分享关于键盘布局的问题,Android弹出键盘布局闪动原理和解决,Android软键盘弹出时把布局顶上去的解决方法。

弹出键盘布局闪动原理和解决

在开发中,遇到一个问题:做一个微信一样,表情输入和软键盘在切换的时候,聊天界面不闪动的问题。为了解决这个问题,需要知道一下Android的软键盘弹出的时候发生的几个变化。

当AndroidMainfest.xml 中配置android:windowSoftInputMode="adjustResize|stateHidden" 属性后,如果弹出软键盘,那么会重绘界面。基本流程如下(API 10):

1.  Android 收到打开软键盘命令

2.  Android 打开软键盘后,调用App 注册在AWM 中的接口,告知它,界面需要进行变化.

2.1 调用ViewRoot.java#W#resized(w,h)

2.2 调用viewRoot.dispatchResized()

2.3 构造Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED),然后post过去

3. 在RootView的handleMessage的case RESIZED_REPORT: 收到具体的大小,配置App Window的大小,特别是 bottom 的大小, 最后调用requestLayout进行重绘

在上述的过程中,我们可以发现,其实弹出键盘之后的界面闪动的核心是在于app 的window botton 属性进行变化了,从而导致整个ViewTree的高度变化。那么我们只要表情PANEL在父布局onMeasure之前,进行VISIBLE或者GONE处理,使得最终的所有子View的高度满足window height,既可以实现不闪动聊天页面。

其核心思想为:在父布局onMeasure之前,隐藏/显示 合适高度的VIEW,既可以使得其他子VIEW高度不变化,从而避免界面闪动。引用自http://blog.dreamtobe.cn/2015/09/01/keyboard-panel-switch/。


代码如下:

Layout:


<com.test.MyActivity.MyLineLayout xmlns:android="http://schemas.android.com/apk/res/android"
                       android:orientation="vertical"
                       android:id="@+id/mll_main"
                       android:layout_width="match_parent"
                       android:layout_height="match_parent">
   <FrameLayout
            android:background="#f3f3f3"
            android:id="@+id/fl_list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">
       <TextView
                android:gravity="center"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="DARKGEM"/>
   
   <LinearLayout
            android:id="@+id/ll_edit"
            android:layout_width="match_parent"
            android:orientation="horizontal"
            android:layout_height="50dp">
       <EditText
                android:id="@+id/et_input"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="match_parent"/>
       <Button
                android:id="@+id/btn_trigger"
                android:text="trigger"
                android:layout_width="100dp"
                android:layout_height="match_parent"/>
   
   <FrameLayout
            android:id="@+id/fl_panel"
            android:background="#CCCCCC"
            android:layout_width="match_parent"
            android:layout_height="0dp"/>



Activity:


package com.test;

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

/**
 *

 * 聊天界面布局闪动处理, 基本原理如下:
 *          1. 弹出键盘的时候,会导致 RootView 的bottom 变小,直到容纳 键盘+虚拟按键
 *          2. 收回键盘的时候,会导致 RootView的bottom 变大,直到容纳 虚拟键盘
 *          3. 因为RootView bottom的变化,会导致整个布局高度(bottom - top)的变化,所以就会发生布局闪动的情况. 而为了
 *          避免这种情况,只需要在发生变动的父布局调用 onMeasure() 之前,将子View的高度和配置为最终高度,既可以实现弹
 *          出/收回键盘 不闪动的效果(如微信聊天界面)。
 *


 */
public class MyActivity extends Activity {
    MyLineLayout mll_main;
    FrameLayout fl_list;
    LinearLayout ll_edit;
    EditText et_input;
    Button btn_trigger;
    FrameLayout fl_panel;
    Rect rect = new Rect();

    enum State {
        //空状态
        NONE,
        //打开输入法状态
        KEYBOARD,
        //打开面板状态
        PANEL,
    }

    State state = State.NONE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mll_main = (MyLineLayout) findViewById(R.id.mll_main);
        fl_list = (FrameLayout) findViewById(R.id.fl_list);
        ll_edit = (LinearLayout) findViewById(R.id.ll_edit);
        et_input = (EditText) findViewById(R.id.et_input);
        btn_trigger = (Button) findViewById(R.id.btn_trigger);
        fl_panel = (FrameLayout) findViewById(R.id.fl_panel);
        mll_main.onMeasureListener = new MyLineLayout.OnMeasureListener() {
            /**
             * 可能会发生多次 调用的情况,因为存在 layout_weight 属性,需要2次测试,给定最终大小
             * */
            @Override
            public void onMeasure(int maxHeight, int oldHeight, int nowHeight) {
                switch (state) {
                    case NONE: {

                    }
                    break;
                    case PANEL: {
                        //state 处于 panel 状态只有一种可能,就是主动点击切换到panel,
                        //1.如果之前是keyboard状态,则在本次onMeasure的时候,一定要把panel显示出来
                        //避免 mll 刷动
                        //2. 如果之前处于 none状态,那么本次触发来自于 postDelay,可以忽略
                        fl_panel.setVisibility(View.VISIBLE);
                    }
                    break;
                    case KEYBOARD: {
                        //state = KEYBOARD 状态,只有一种可能,就是主动点击了 EditText
                        //1. 如果之前是panel状态,则一般已经有了固有高度,这个高度刚刚好满足键盘的高度,那么只用隐藏掉
                        //panel 既可以实现页面不进行刷新
                        //2. 如果之前为none状态,则可以忽略
                        fl_panel.setVisibility(View.GONE);
                        //处于键盘状态,需要更新键盘高度为面板的高度
                        if (oldHeight >= nowHeight) {
                            //记录当前的缩放大小为键盘大小
                            int h = maxHeight - nowHeight;
                            //避免 输入法 悬浮状态, 保留一个最低高度
                            if (h < 500) {
                                h = 500;
                            }
                            fl_panel.getLayoutParams().height = h;
                        }
                    }
                    break;
                }
                Log.d("SC_SIZE", String.format("onMeasure %d %d %d", maxHeight, nowHeight, oldHeight));
            }
        };
        fl_list.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                hideSoftInputView();
                fl_panel.setVisibility(View.GONE);
                state = State.NONE;
                return false;
            }
        });
        et_input.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                state = State.KEYBOARD;
            }
        });
        btn_trigger.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (state) {
                    case NONE:
                    case KEYBOARD: {
                        hideSoftInputView();
                        state = State.PANEL;
                        //无论App 处于什么状态,都追加一个 显示 panel 的方法,避免处于非正常状态无法打开panel
                        getWindow().getDecorView().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                fl_panel.setVisibility(View.VISIBLE);
                            }
                        }, 100);
                    }
                    break;
                    case PANEL: {
                        state = State.NONE;
                        fl_panel.setVisibility(View.GONE);
                    }
                    break;
                }
            }
        });
        //设置基本panel 高度,以使得第一次能正常打开panel
        getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        fl_panel.getLayoutParams().height = rect.height() / 2;
        fl_panel.setVisibility(View.GONE);
    }

    /**
     * 隐藏软键盘输入
     */
    public void hideSoftInputView() {
        InputMethodManager manager = ((InputMethodManager) this.getSystemService(Activity.INPUT_METHOD_SERVICE));

        if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
            if (getCurrentFocus() != null && manager != null)
                manager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }

    /**
     * Created by Administrator on 2015/11/20.
     */
    public static class MyLineLayout extends LinearLayout {
        OnMeasureListener onMeasureListener;
        int maxHeight = 0;
        int oldHeight;

        public MyLineLayout(Context context) {
            super(context);
        }

        public MyLineLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int height = MeasureSpec.getSize(heightMeasureSpec);
            if (onMeasureListener != null) {
                onMeasureListener.onMeasure(maxHeight, oldHeight, height);
            }
            oldHeight = height;
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            //之所以,在这里记录 maxHeight的大小,是因为 onMeasure 中可能多次调用,中间可能会逐步出现 ActionBar,BottomVirtualKeyboard,
            //所以 onMeasure中获取的maxHeight存在误差
            if (h > maxHeight) {
                maxHeight = h;
            }
            Log.d("SC_SIZE", String.format("Size Change %d %d", h, oldh));
        }

        interface OnMeasureListener {
            void onMeasure(int maxHeight, int oldHeight, int nowHeight);
        }
    }
}


测试效果图:




解决Andriod软键盘出现把原来的布局给顶上去的方法

决方法,在mainfest.xml中,对那个Activity加:




就不会把原来Activity的布局给顶上去了。

今天要做一个搜索功能,搜索界面采用AutoCompleteTextView做搜索条,然后下面用listview来显示搜索结果,而我的主界面是在底部用tab做了一个主界面导航,其中有一个搜索按钮,因为在搜索条中输入文字的时候会弹出软件盘,但是如果不做什么设置的话,软键盘弹出来的同时,会把我下面的tab导航给相应拉到屏幕的上面,界面显示的扭曲啊,后来找到一种解决方法,在相应的activity中(比如我这是tab的activity,用的是adjustpan)添加

android:windowSoftInputMode这个属性,下面详细说下这个属性:

& X! Q6c9 }% i. ]6 @0 Y" N6^  d  {"X

windowSoftInputMode属性设置值说明。





attributes:

android:windowSoftInputMode

活动的主窗口如何与包含屏幕上的软键盘窗口交互。这个属性的设置将会影响两件事情7S7 U+ S! p7 s( U) n: t: m& N
:

1>   
软键盘的状态——是否它是隐藏或显示——当活动5w$ r- U9 i" h. O' M" M
(Activity)成为用户关注的焦点。

2>   
活动的主窗口调整——是否减少活动主窗口大小以便腾出空间放软键盘或是否当活动窗口的部分被软键盘覆盖时它的内容的当前焦点是可见的。

它的设置必须是下面列表中的一个值,或一个
”state…”值加一个+ s. Z" m5 u: {; k; B7v4 Q
”adjust…”值的组合。在任一组设置多个值——多个
”state…”values,例如&
mdash有未定义的结果。各个值之间用+H8 v$ Q# ~5 f3 B& `- G8 c$ y
|分开。例如
:

在这设置的值8A: N! L' x0 `: C
(除'H0 N" g, w2 W) K  F# y2 l!c
"stateUnspecified"和
"adjustUnspecified"以外3 ^, p2E  G: I2 y/ V
)将覆盖在主题中设置的值


将覆盖在主题中设置的值

描述

"stateUnspecified"

软键盘的状态
(是否它是隐藏或可见
)没有被指定。系统将选择一个合适的状态或依赖于主题的设置。这个是为了软件盘行为默认的设置。

"stateUnchanged"

软键盘被保持无论它上次是什么状态,是否可见或隐藏,当主窗口出现在前面时。

"stateHidden"

当用户选择该
Activity时,软键盘被隐藏——也就是,当用户确定导航到该# ^* {1 w. C: Y-}
Activity时,而不是返回到它由于离开另一个: }/ N! p( a* M% W. b%m9 b# u
Activity

"stateAlwaysHidden"

软键盘总是被隐藏的,当该
Activity主窗口获取焦点时。

"stateVisible"

软键盘是可见的,当那个是正常合适的时& d% G.y8 [; G; _: v
(当用户导航到" s2 I/ J$ s2 w7 J) j6O& J, f
Activity主窗口时
)

"stateAlwaysVisible"

当用户选择这个6 Z%C  e/ r0 u,M  `7N  r
Activity时,软键盘是可见的——也就是,也就是,当用户确定导航到该. m; r6 r% t. `# A4 C0^, t; d! V
Activity时,而不是返回到它由于离开另一个
Activity

"adjustUnspecified"

它不被指定是否该" H9 b! V3 h5_& O$ d$ M  B" V5j: d
Activity主窗口调整大小以便留出软键盘的空间,或是否窗口上的内容得到屏幕上当前的 焦点是可见的。系统将自动选择这些模式中一种主要依赖于是否窗口的内容有任何布局视图能够滚动他们的内容。如果有这样的一个视图,这个窗口将调整大小,这 样的假设可以使滚动窗口的内容在一个较小的区域中可见的。这个是主窗口默认的行为设置。

"adjustResize"

/ M" R: m- W( Z. Q6 d*A
Activity主窗口总是被调整屏幕的大小以便留出软键盘的空间

"adjustPan"


Activity主窗口并不调整屏幕的大小以便留出软键盘的空间。相反,当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分。这个通常是不期望比调整大小,因为用户可能关闭软键盘以便获得与被覆盖内容的交互操作。


在Android应用开发中,滚动嵌套滑动经常用到,本文我们具体讲讲ScrollView滚动机制,以及NestedScrolling嵌套滑动机制。

Android ScrollView滚动机制


我们都知道通过View#scrollTo(x,y)既可以实现将View滚动的效果,如果再添加Scroller类,就可以实现滚到效果。但是,这背后是如何实现的呢?这个问题涉及到View的绘图机制。我们先看看View的绘图的基本流程


(图片来自于网上比较常见的view绘图流程图)

关于三个阶段的简单描述:

1. measure:预估计ViewTree的各个View的占用空间。
2. layout :   确定ViewTree中各个View所处的空间位置,包括width,height,left,top,right,bottom
3. draw:  使用RootViewImpl中的一个surface.lockCanvas(dirty)对象作为画布,然ViewTree上所有的View都在这个Canvas上进行画图,

值得注意的是,Canvas通过getHeight() 和 getWidth()就是整个屏幕的真实大小。包括了通知栏(虽然在打印出来的ViewTree看不到,但是通过top属性,留下了一点空间给通知栏),标题栏,Content,底部虚拟按键等。

我们先看看mScrollX/mScrollY在代码中的注释:


mScrollX/mScrollY相对这个View的内容(文字,图片,子View)垂直/水平的像素偏移。如下图:


在设置mScrollX / mScrollY后,就可以滚动到指定的“内容",而mScrollX/mScrollY 就是相对于“内容”的偏移量,内容原点为(0,0)。

而这种内容大小以及偏移是如何发生的?在ViewGroup中,存在一个API drawChild(),这个函数主要完成对子View的空间大小的限制以及偏移,见如下的描述

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    boolean more = false;
    //获取子View的空间大小
    final int cl = child.mLeft;
    final int ct = child.mTop;
    final int cr = child.mRight;
    final int cb = child.mBottom;
    //通知子View进行判断是否完成滚动,这里就是通过Scroller代码实现滚动的关键点
    child.computeScroll();
    //获取最新的偏移量
    final int sx = child.mScrollX;
    final int sy = child.mScrollY;
    //创建一个还原点
    final int restoreTo = canvas.save();
    //偏移,通过这个API,实现了scroll对内容偏移, 先把内容的原点进行偏移到负数区域
    canvas.translate(cl - sx, ct - sy);
    //剪切,因为之前有一个translate操作,所有剪切出来的空间就是父View给定的可见区域
    //所以如果子View填充Canvas的内容超出给定的空间,也不会显示出来
    canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
    //让子View进行绘图,注意子View不用处理Scroll属性,既可以实现内容偏移
    child.draw(canvas);
    //还原
    canvas.restoreToCount(restoreTo);
    return more;
}



值得注意的是,ListView不是采用这种机制实现的,而是采用替换ChildView来实现滑动效果的。



Android 嵌套滑动机制(NestedScrolling)

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性

NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView。

效果如上图


在这之前,我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

这种分发机制有一个漏洞:

    如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下

    NestedScrollingChild
    NestedScrollingParent
    NestedScrollingChildHelper
    NestedScrollingParentHelper


以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout)

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }
    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }


对,简单的话你就这么实现就好了。

这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法

    /**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link NestedScrollingChild} interface method with the same signature to implement
     * the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link NestedScrollingChild#startNestedScroll(int)}.
     * @return true if a cooperating parent view was found and nested scrolling started successfully
     */
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:


一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent


二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。


三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。


四、stopNestedScroll

结束整个流程。

整个对应流程是这样


子view父view
startNestedScrollonStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScrollonNestedPreScroll
dispatchNestedScrollonNestedScroll
stopNestedScrollonStopNestedScroll


一般是子view发起调用,父view接受回调。

我们最需要关注的是dispatchNestedPreScroll中的consumed参数。

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。

对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。


Layout_weight是Android开发中常用布局属性,本文详细介绍了Layout_weight的属性,它是用来分配属于空间的一个属性,你可以设置他的权重。

Layout_weight是Android开发中一个比较常用的布局属性,在面试中也经常被问到.下面通过实例彻底搞懂Layout_weight的用法.

先看下面的布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_weight="1"
        android:background="#44ff0000"
        android:gravity="center"
        android:text="111111111111"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_weight="2"
        android:background="#4400ff00"
        android:gravity="center"
        android:text="2"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_weight="3"
        android:background="#440000ff"
        android:gravity="center"
        android:text="3"/>
</LinearLayout>

在水平方向的线性布局中,有三个TextView,layout_width都是0dp,layout_weight分别为1,2,3,所以这三个TextView应该按1:2:3的比例分布,

   显示效果:

  


  可以看到,虽然三个TextView是按1:2:3的比例进行分布的,但是第一个TextView却没有和另外两个对齐,这是为什么呢?

  仔细看就能发现,虽然三个TextView不是对齐的,但是第一行的文本是对齐的.

 

  我们只需将LinearLayout的baselineAligned属性设置为false即可,baselineAligned表示是否以文本基准线对齐.

  
  这只是对layout_weight属性最基本的用法.假如把第一个TextView的layout_width该为wrap_content,会出现什么情况呢?


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:baselineAligned="false">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:layout_weight="1"
        android:background="#44ff0000"
        android:gravity="center"
        android:text="111111111111"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_weight="2"
        android:background="#4400ff00"
        android:gravity="center"
        android:text="2"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_weight="3"
        android:background="#440000ff"
        android:gravity="center"
        android:text="3"/>
</LinearLayout>

  看下效果:

  


  发现三个TextView并没有再按照1:2:3的比例进行分配.其实,layout_weight属性是先按控件声明的尺寸进行分配,然后将剩余的尺寸按layout_weight的比例进行分配.  

  设屏幕总宽度为sum,第一个TextView声明的尺寸为x,那么,

  第一个TextView的宽度为: x + (sum - x) * 1/6

  第二个TextView的宽度为: (sum - x) * 2/6

  第三个TextView的宽度为: (sum - x) * 3/6

  为了证实这个结论,接下来继续修改布局,将三个TextView的layout_width都设置为match_parent,把layout_weight分别设置为1,2,2


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:baselineAligned="false">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_weight="1"
        android:background="#44ff0000"
        android:gravity="center"
        android:text="111111111111"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_weight="2"
        android:background="#4400ff00"
        android:gravity="center"
        android:text="2"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_weight="2"
        android:background="#440000ff"
        android:gravity="center"
        android:text="3"/>
</LinearLayout>

  先看下效果:

  


  看到这个结果可能会感到很诧异,第一个TextView的layout_weight明明比其它两个的小,为什么宽度却比它们大呢?

  我们按刚才得出的结论算一下.

  依然设屏幕总宽度为sum,由于这三个TextView声明的尺寸都是match_parent,也就是sum,那么,

  第一个TextView的宽度为: sum + (sum - 3*sum) * 1/5 = sum*3/5

  第二个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5

  第三个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5

  三个TextView的宽度比例为 3:1:1,所以我们的结论是正确的.    

  需要注意的是,我们结论所说的剩余的尺寸可能是负的,如 sum - 3*sum,

  另外,通过最后这个例子可以看出,并不是layout_weight越大,宽度越大.



Android Layout_weight的深刻理解

首先看一下Layout_weight属性的作用:它是用来分配属于空间的一个属性,你可以设置他的权重。很多人不知道剩余空间是个什么概念,下面 我先来说说剩余空间。

看下面代码:
 

    <?xml version="1.0" encoding="utf-8"?>     

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     

        android:orientation="vertical"     

        android:layout_width="fill_parent"     

        android:layout_height="fill_parent"     

        >     

    <EditText     

        android:layout_width="fill_parent"     

        android:layout_height="wrap_content"     

        android:gravity="left"     

        android:text="one"/>     

    <EditText     

        android:layout_width="fill_parent"     

        android:layout_height="wrap_content"     

        android:gravity="center"     

        android:layout_weight="1.0"     

        android:text="two"/>     

        <EditText     

        android:layout_width="fill_parent"     

        android:layout_height="wrap_content"     

        android:gravity="right"     

        android:text="three"/>     

    </LinearLayout>     

运行结果是:

\
看上面代码:只有Button2使用了Layout_weight属性,并赋值为了1,而Button1和Button3没有设置 Layout_weight这个属性,根据API,可知,他们默认是0

下面我就来讲,Layout_weight这个属性的真正的意思:Android系统先按照你设置的3个Button高度 Layout_height值wrap_content,给你分配好他们3个的高度,

然后会把剩下来的屏幕空间全部赋给Button2,因为只有他的权重值是1,这也是为什么Button2占了那么大的一块空间。

有了以上的理解我们就可以对网上关于Layout_weight这个属性更让人费解的效果有一个清晰的认识了。

我们来看这段代码:

 

     <?xml version="1.0" encoding="UTF-8"?>   

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   

        android:layout_width="fill_parent"   

        android:layout_height="wrap_content"   

        android:orientation="horizontal" >   

        <TextView   

            android:background="#ff0000"   

            android:layout_width="**"   

            android:layout_height="wrap_content"   

            android:text="1"   

            android:textColor="@android:color/white"   

            android:layout_weight="1"/>   

        <TextView   

            android:background="#cccccc"   

            android:layout_width="**"   

            android:layout_height="wrap_content"   

            android:text="2"   

            android:textColor="@android:color/black"   

            android:layout_weight="2" />   

         <TextView   

            android:background="#ddaacc"   

            android:layout_width="**"   

            android:layout_height="wrap_content"   

            android:text="3"   

            android:textColor="@android:color/black"   

            android:layout_weight="3" />   

    </LinearLayout>

三个文本框的都是 layout_width=“wrap_content ”时,会得到以下效果

\

按照上面的理解,系统先给3个TextView分配他们的宽度值 wrap_content(宽度足以包含他们的内容1,2,3即可),然后会把剩下来的屏幕空间按照1:2:3的比列分配给3个textview,所以就 出现了上面的图像。

而当layout_width=“fill_parent”时, 如果分别给三个TextView设置他们的Layout_weight为1、2、2的话,就会出现下面的效果:

\

你会发现 1的权重小,反而分的多了,这是为什么呢???网上很多人说是当layout_width=“fill_parent”时,weighth 值越小权重越大,优先级越高,就好像在背口诀

一样,其 实他们并没有真正理解这个问题,真正的原因是Layout_width="fill_parent"的原因造成的。依照上面理解我们来分析:

系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度

那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )

那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/5 * 剩余空间大小(-2 parent_width)=3/5parent_width

同理第二个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;

第三个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;所以就是3:1:1的比列显示了。

这样你也就会明白为什么当你把三个Layout_weight设置为1、2、3的话,会出现下面的效果了:

\

第三个直接不显示了,为什么呢?一起来按上面方法算一下吧:

系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度

那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )

那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/6 * 剩余空间大小(-2 parent_width)=2/3parent_width

同理第二个TextView的实际所占宽度=parent_width + 2/6*(-2parent_width)=1/3parent_width;

第三个TextView的实际所占宽度=parent_width + 3/6*(-2parent_width)=0parent_width;所以就是2:1:0的比列显示了。第三个就直接没有空间了。

AndBase是为Android开发者量身打造的一款开源类库产品,AndBase框架的藕合性非常的低,你可以在你的项目中用完整的源码进行编译使用。

PullToRefrech下拉刷新..这个效果在绝大多数使用app貌似都使用的到..一个页面的、如果数据太多必然进行下拉..那么下拉之后就需要显示新的数据信息..这就是下拉刷新的完整体现.也可以说这种效果能够给用户一个更加良好的体验..那么我们看看下拉刷新到底是如何实现的..

1.普通View的PullReferch

  无论是普通View的下拉刷新还是ListView,还是GridView..下拉刷新实现的原理基本都是相同的..只不过实现上有一些简单的细节不太一样而已...首先先看看普通View的下拉刷新..实现普通下拉刷新需要使用到AbPullView, AbPullView extends ScollView 可滑动视图...不难想象的一件事情就是无论是怎样进行滑动..其实都是对布局的覆盖..

  手机的屏幕大小是有限的...因此无论是怎样进行滑动..都是一层布局去覆盖原来的那层布局..原来的那层布局会被直接销毁..如果按照正常思路去想,那么就算是View被销毁了...那么自然有记录了这次布局的相关数据...当这些被销毁的布局需要再次显示的时候,那么直接调用记录数据就可以了..没有必要再建立新的布局..由于是AndBase内部把这个视图已经封装好了因此我们需要进行相关的引用..说白了就是需要进行动态的布局...因此布局文件中,需要这样去进行书写..我们通过获取ID的方式进行动态的布局..

  <com.ab.view.pullview.AbPullView
        android:id="@+id/mPullView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
  </com.ab.view.pullview.AbPullView>

  获取到了ID之后..通过动态加载xml数据..就可以设置好下拉需要显示的相关View..设置的方式其实就是一行代码就能解决的事情..

  mAbPullView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));

  这个方式就是设置顶部下拉刷新的一个背景显示...这里我设置了一个动态的效果...R.drawable.progress_circular...


<!-- 单帧动画旋转功能的实现  -->
<!-- 持续时间:两秒
          起始角度:0度
          定义动画的旋转样式..起始和结束都减速..中间过程加速..
          偏移量:顶部和左部
          重复的样式:重新开始
          末端角度:360度
          背景设置:drawable-->
<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200"
    android:fromDegrees="0.0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:pivotX="50.0%"
    android:pivotY="50.0%"
    android:repeatMode="restart"
    android:toDegrees="360.0"
    android:drawable="@drawable/progress_loading2">
</animated-rotate>



  上面的这些过程都是设置下拉时需要显示的View数据..并没有真正涉及到下拉的过程..说白了这个就是下拉后需要发生的事情..而真正想要实现下拉..少不了监听事件的设置..有了对下拉的监听..系统才能够对下拉进行相关的响应..响应事件的绑定..

 mAbPullView.setAbOnRefreshListener(new AbOnRefreshListener(){
    @Override
    public void onRefresh() {
        mAbTaskQueue.execute(item); //下拉时需要执行的事件..
    }      
 });

  绑定上了监听之后..当我们在屏幕上进行下拉的时候..那么就会有事件进行响应了..这里我设置任务是一个异步线程任务...我们可以在这个任务中设置需要执行的事件..然后将事物放入到任务执行队列当中..那么任务就会在下拉的这个时间段内被执行..然后更新在UI界面上..


final AbTaskItem item = new AbTaskItem();
        item.listener = new AbTaskListener() {

            @Override
            public void update() {
                                //当任务完成之后..那么停止刷新..也就表示下拉时出现的视图需要被隐藏了...
                removeProgressDialog();
                mAbPullView.stopRefresh(); //停止刷新..
            }

            @Override
            public void get() {
                   try {
                       Thread.sleep(1000);
                       //这里是任务执行的过程...
                   } catch (Exception e) {
                   }
          };
        };



  这样通过几个简单的过程就可以完成普通View的下拉刷新...这个的源码我并没有进行特别深入的研究...其实关键地方就是ScollView 和 FrameLayout..总体的思想我也说过了..视图无论是滑动还是下拉刷新原理就是这样...就是View的覆盖..如果有更有兴趣的..可以去研究研究这两个类的源码...

2.ListView下拉刷新的实现...

  ListView下拉刷新,这个在绝大多数的app中都必然会使用到..就拿我们的博客园app来说吧..博客的放入在屏幕上的显示是有限的..那么如果我们想要看到更多的博客文章..那么无非需要下拉刷新加载更多数据,或者是使用分页的方式去显示更多的数据..博客园的app是下拉刷新来实现的..用过的人还是非常清楚的..因此ListView的下拉刷新还是非常的重要的..必须要进行掌握的...

  AndBase仍然把一些类进行了封装..提供给我们进行使用..去实现ListView的下拉刷新..原理基本是相同的..需要使用AbPullListView extends ListView implements OnScollListener在继承了ListView的情况下同时也就实现了滚动监听..下面这个接口是在为ListView设置监听后需要实现的方法...方法的实现由我们去实现..这个类仅仅提供了在下拉或上啦后需要做的事情..并没有涉及到滑动的过程...


public interface AbOnListViewListener {
    
    /**
     * On refresh.
     */
    public void onRefresh();

    /**
     * On load more.
     */
    public void onLoadMore();
}



  滑动的过程是由于AbPullListView实现了OnTouchEvent方法..OnTouchEvent表示触摸事件监听...当在屏幕上发生按下,抬起,移动等操作时需要执行..这也就是对滑动事件的一个监听设置..没了这个方法..我们怎么滑动屏幕都是徒劳..那么当发生了上拉或者是下拉就会去执行相应的监听事件..这个上拉或者是下拉的过程是通过滑动的坐标值来确定的..这里就涉及到了OnTouchEvent的源码..源码的实现也比较简单...大体的东西还是比较清晰的..


@Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }

        switch (ev.getAction()) {  //对事件的判断..
        case MotionEvent.ACTION_DOWN:  //按下操作..
            mLastY = ev.getRawY();       //获取数值坐标..
            break;
        case MotionEvent.ACTION_MOVE:   //移动操作..
            final float deltaY = ev.getRawY() - mLastY;  //原始值减掉最终值..
            mLastY = ev.getRawY();        //获取原始值..
            if (mEnablePullRefresh && getFirstVisiblePosition() == 0 && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) {  //这里是对坐标的判断过程..if条件表示满足上拉条件..
                updateHeaderHeight(deltaY / OFFSET_RADIO);
            } else if (mEnablePullLoad && !mPullLoading && getLastVisiblePosition() == mTotalItemCount - 1 && deltaY<0) {  //这个表示下拉过程..
                startLoadMore();
            }
            break;
        case MotionEvent.ACTION_UP:  //抬起操作的执行过程..
            mLastY = -1;
            if (getFirstVisiblePosition() == 0) {
                //需要刷新的条件
                if (mEnablePullRefresh && mHeaderView.getVisiableHeight() >= mHeaderViewHeight) {
                    mPullRefreshing = true;
                    mHeaderView.setState(AbListViewHeader.STATE_REFRESHING);
                    if (mListViewListener != null) {
                        //刷新
                        mListViewListener.onRefresh();
                    }
                }
                
                if(mEnablePullRefresh){
                    //弹回
                    resetHeaderHeight();
                }
            }
            break;
        default:
            break;
        }
        return super.onTouchEvent(ev);
    }



  那么有了触摸事件的监听之后..当触摸事件发生之后就可以执行相关的监听事件..这样就可以去执行下拉和上拉过程中需要执行的任务了...那么现在就说一下ListView实现下拉刷新的过程...它的布局文件其实也是和普通View的下拉定义一样..


   <com.ab.view.pullview.AbPullListView
        android:id="@+id/mListView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:cacheColorHint="#00000000"
        android:divider="@drawable/list_divider"
        android:dividerHeight="1dip"
        android:fastScrollEnabled="true">
   </com.ab.view.pullview.AbPullListView>



  我们需要获取它的ID信息..然后再进行动态布局..这就是ListView的刷新过程..首先我们需要允许它能够进行下拉和上拉的操作..否则是无法滑动的..然后设置下拉时显示的东西..我这里是定义了一个动画效果..表示当前的这个View中的东西要实现旋转效果..效果就是普通View中定义的那个效果..


  //获取ListView对象
  mAbPullListView = (AbPullListView)this.findViewById(R.id.mListView);
        
  //打开关闭下拉刷新加载更多功能
  mAbPullListView.setPullRefreshEnable(true);
  mAbPullListView.setPullLoadEnable(true);
        
  //设置进度条的样式
  mAbPullListView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
  mAbPullListView.getFooterView().setFooterProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));



  然后需要设置相关的监听事件...监听事件就需要重写这两个方法了..前面已经涉及到了这两个方法..就不再进行细说了..一个方法表示的是上拉过程需要进行重新刷新视图,一个表示的是下拉刷新需要加载更多的视图..两个方法..我们需要自定义去实现..


mAbPullListView.setAbOnListViewListener(new AbOnListViewListener(){

            @Override
            public void onRefresh() {
                mAbTaskQueue.execute(item1);
            }

            @Override
            public void onLoadMore() {
                mAbTaskQueue.execute(item2);
            }
            
        });



  我自己定义了两个异步任务..这两个异步任务用于对数据进行下载的一个操作..那么数据在下载之后是需要更新到ListView中的Item上的..那么Item的数据更新就需要适配器来完成了..因此ListView的适配器不可或缺的一个重要部分..在适配器内部去设置相关样式的显示..当发生下拉或者是上拉等操作的时候..我们去唤醒适配器去更新数据就可以了..更新数据的方法调用..

 myListViewAdapter.notifyDataSetChanged();

  在这里我只简单的说了一下原理...这个就是任务代码的设置..


final AbTaskItem item1 = new AbTaskItem();
        item1.listener = new AbTaskListener() {

            @Override
            public void update() {
                removeProgressDialog();
                list.clear();
                if(newList!=null && newList.size()>0){
                    list.addAll(newList);
                    myListViewAdapter.notifyDataSetChanged();
                    newList.clear();
                   }
                mAbPullListView.stopRefresh();
            }
            @Override
            public void get() {
                   try {
                       Thread.sleep(1000);
                       currentPage = 1;
                       newList = new ArrayList<Map<String, Object>>();
                       Map<String, Object> map = null;
                       
                       for (int i = 0; i < pageSize; i++) {
                           map = new HashMap<String, Object>();
                           map.put("itemsIcon",mPhotoList.get(new Random().nextInt(mPhotoList.size())));
                           map.put("itemsTitle", "item"+(i+1));
                           map.put("itemsText", "item..."+(i+1));
                           newList.add(map);
                           
                       }
                   } catch (Exception e) {
               }
          };
        };



  总体的过程就是这样..想要做出更好的效果..我们就需要放入我们自己的思想..就能够做出更好的下拉效果了...

3.GridView 下拉刷新..

  GridView网格视图..视图以网格的形式进行展现..每一个网格都是一个子视图..原理和ListView基本相同..只不过ListView是一行行的显示..GridView是一块块的去显示..少不了适配器的使用..只要我们获取到网格中的子选项..然后为每一个子选项设置样式..就可以完成一个网格View的设置...网格View的使用也应该不少..只要我们的设置合理..同样能够做出非常漂亮的网格视图..

  GridView采用的方式就不是FrameLayout了..它使用的是线性布局..AbPullGridView extends AbBaseGridView implements OnScollListener, OnTouchListener.. AbBaseGridView是继承了LinearLayout..原理采用线性布局的方式将视图进行展示..通过实现滚动监听和触摸监听就能够实现滚动..以及触摸事件的监听设置..有兴趣的还是可以去研究一下源码的实现过程..我就只说一下其中的调用方式..

  布局的定义方式仍然是差不多的..仍然是获取ID,然后通过动态加载的方式进行数据的加载..

<com.ab.view.pullview.AbPullGridView
        android:id="@+id/mPhotoGridView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
<com.ab.view.pullview.AbPullGridView/>

   调用的过程和ListView基本相同..


mAbPullGridView = (AbPullGridView)findViewById(R.id.mPhotoGridView);  
        
 //开关默认打开
mAbPullGridView.setPullRefreshEnable(true);
mAbPullGridView.setPullLoadEnable(true);
        
//设置进度条的样式
mAbPullGridView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
mAbPullGridView.getFooterView().setFooterProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
        
mGridView = mAbPullGridView.getGridView();  
mGridView.setColumnWidth(150);          //设置列的宽度..
mGridView.setGravity(Gravity.CENTER);   //对齐方式为中央对其..
mGridView.setHorizontalSpacing(5);     //设置水平间距..
mGridView.setNumColumns(GridView.AUTO_FIT);    //列的数量为自适应屏幕为限制..
mGridView.setPadding(5, 5, 5, 5);             //设置间距..
mGridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);  
mGridView.setVerticalSpacing(5);              //GridView之间的垂直间距..



  只是GridView的子选项需要我们设置更多的东西..需要设置子选项的排列以及显示方式..设置好了一个子选项的排列方式,那么其他所有的子选项会按照这同一种方式显示在屏幕之上..这样只是设置好了排布的问题..具体需要显示的东西我们还是需要通过使用适配器来安排子选项需要显示的东西..

  同样滑动之后需要执行的监听事件我们是需要进行重写的..


    mAbPullGridView.setAbOnListViewListener(new AbOnListViewListener() {
            
            @Override
            public void onRefresh() {
                //第一次下载数据
                mAbTaskQueue.execute(item1);
            }
            
            @Override
            public void onLoadMore() {
                mAbTaskQueue.execute(item2);
            }
        });



  同时我们也可以设置子选项被点击时设置的相关监听..通过设置子选项的监听..可以通过触发子选项执行更多的操作..忘记说了..ListView也是可以触发子选项的监听事件的..比较简单就没必要再贴代码了...


mAbPullGridView.getGridView().setOnItemClickListener(new OnItemClickListener(){

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                showToast(""+position);
            }
            
        });
        



  这里的异步任务我也就不再进行粘贴了..和ListVIew基本都是相同的..当任务执行完毕之后..通过唤醒适配器去更新数据信息就可以完成了..这样就实现了PullToRefrech下拉刷新..我们知道了调用过程之后..就能够通过自己定义的方式去设置更加良好的刷新效果..调用的过程不是很难..真正难的是我们自己的思想..如何设置出更加良好的UI才是更加重要的..



Android-PullToRefresh下拉刷新库基本用法
 
PullToRefresh是一套实现非常好的下拉刷新库,它支持:
ListView
ExpandableListView
GridView
WebView
ScrollView
HorizontalScrollView
ViewPager
等多种常用的需要刷新的View类型,而且使用起来也十分方便。
(下载地址:https://github.com/chrisbanes/Android-PullToRefresh)

PullToRefresh基本用法:
1、在布局文件中添加PullToRefresh控件,比如PullToRefreshListView;
2、在Activity中,设置监听器OnRefreshListener以响应用户下拉操作;
3、在监听器的onRefresh()方法中执行数据刷新操作,可以通过AsyncTask来实现;
4、在AsyncTask中获取到数据后,记得调用onRefreshComplete()方法通知PullToRefresh控件数据已获取完毕,可以结束刷新操作。

实例:PullToRefreshDemo
运行效果:

视图结构


代码清单:

布局文件:activity_main.xml


    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        xmlns:tools="http://schemas.android.com/tools"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:paddingBottom="@dimen/activity_vertical_margin"  
        android:paddingLeft="@dimen/activity_horizontal_margin"  
        android:paddingRight="@dimen/activity_horizontal_margin"  
        android:paddingTop="@dimen/activity_vertical_margin"  
        tools:context=".MainActivity" >  
       
        <com.handmark.pulltorefresh.library.PullToRefreshListView  
            android:id="@+id/pull_to_refresh_listview"  
            android:layout_height="fill_parent"  
            android:layout_width="fill_parent" />  
      
    </RelativeLayout>  


Java源代码文件:MainActivity.java


    package com.rainsong.pulltorefreshdemo;  
      
    import java.util.Arrays;  
    import java.util.LinkedList;  
      
    import android.app.Activity;  
    import android.os.AsyncTask;  
    import android.os.Bundle;  
    import android.text.format.DateUtils;  
    import android.view.Menu;  
    import android.widget.ArrayAdapter;  
    import android.widget.ListView;  
      
    import com.handmark.pulltorefresh.library.PullToRefreshBase;  
    import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;  
    import com.handmark.pulltorefresh.library.PullToRefreshListView;  
      
    public class MainActivity extends Activity {  
        private PullToRefreshListView mPullToRefreshListView;  
        private LinkedList<String> mListItems;  
        private ArrayAdapter<String> mAdapter;  
      
        @Override  
        protected void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.activity_main);  
              
            // Set a listener to be invoked when the list should be refreshed.  
            mPullToRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_to_refresh_listview);  
            mPullToRefreshListView.setOnRefreshListener(new OnRefreshListener<ListView>() {  
                @Override  
                public void onRefresh(PullToRefreshBase<ListView> refreshView) {  
                    String label = DateUtils.formatDateTime(getApplicationContext(), System.currentTimeMillis(),  
                            DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL);  
                      
                    // Update the LastUpdatedLabel  
                    refreshView.getLoadingLayoutProxy().setLastUpdatedLabel(label);  
                      
                    // Do work to refresh the list here.  
                    new GetDataTask().execute();  
                }  
            });  
              
            ListView actualListView = mPullToRefreshListView.getRefreshableView();  
              
            mListItems = new LinkedList<String>();  
            mListItems.addAll(Arrays.asList(mStrings));  
              
            mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);  
            actualListView.setAdapter(mAdapter);  
        }  
          
        private class GetDataTask extends AsyncTask<Void, Void, String[]> {  
      
            @Override  
            protected String[] doInBackground(Void... params) {  
                // Simulates a background job.  
                try {  
                    Thread.sleep(4000);  
                } catch (InterruptedException e) {  
                }  
                return mStrings;  
            }  
            @Override  
            protected void onPostExecute(String[] result) {  
                mListItems.addFirst("Added after refresh...");  
                mAdapter.notifyDataSetChanged();  
                  
                // Call onRefreshComplete when the list has been refreshed.  
                mPullToRefreshListView.onRefreshComplete();  
                super.onPostExecute(result);  
            }  
        }  
      
        @Override  
        public boolean onCreateOptionsMenu(Menu menu) {  
            // Inflate the menu; this adds items to the action bar if it is present.  
            getMenuInflater().inflate(R.menu.main, menu);  
            return true;  
        }  
      
        private String[] mStrings = { "John", "Michelle", "Amy", "Kim", "Mary",  
                "David", "Sunny", "James", "Maria", "Michael", "Sarah", "Robert",  
                "Lily", "William", "Jessica", "Paul", "Crystal", "Peter",  
                "Jennifer", "George", "Rachel", "Thomas", "Lisa", "Daniel", "Elizabeth",  
                "Kevin" };  
    } 

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • C#创建自定义控件及添加自定义属性和事件使用实例详解

    这篇文章主要给大家介绍了关于C#创建自定义控件及添加自定义属性和事件使用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-06-25
  • JS实现自定义简单网页软键盘效果代码

    本文实例讲述了JS实现自定义简单网页软键盘效果。分享给大家供大家参考,具体如下:这是一款自定义的简单点的网页软键盘,没有使用任何控件,仅是为了练习JavaScript编写水平,安全性方面没有过多考虑,有顾虑的可以不用,目的是学...2015-11-08
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • JS使用cookie实现DIV提示框只显示一次的方法

    本文实例讲述了JS使用cookie实现DIV提示框只显示一次的方法。分享给大家供大家参考,具体如下:这里运用JavaScript的cookie技术,控制网页上的提示DIV只显示一次,也就是当用户是第一次打开网页的时候才显示,第二次自动隐藏起...2015-11-08
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • 自定义jquery模态窗口插件无法在顶层窗口显示问题

    自定义一个jquery模态窗口插件,将它集成到现有平台框架中时,它只能在mainFrame窗口中显示,无法在顶层窗口显示. 解决这个问题的办法: 通过以下代码就可能实现在顶层窗口弹窗 复制代码 代码如下: $(window.top.documen...2014-05-31
  • 自定义feignClient的常见坑及解决

    这篇文章主要介绍了自定义feignClient的常见坑及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • pytorch 自定义卷积核进行卷积操作方式

    今天小编就为大家分享一篇pytorch 自定义卷积核进行卷积操作方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-05-06
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • PHP YII框架开发小技巧之模型(models)中rules自定义验证规则

    YII的models中的rules部分是一些表单的验证规则,对于表单验证十分有用,在相应的视图(views)里面添加了表单,在表单被提交之前程序都会自动先来这里面的规则里验证,只有通过对其有效的限制规则后才能被提交,可以很有效地保证...2015-11-24
  • jquery自定义插件开发之window的实现过程

    这篇文章主要介绍了jquery自定义插件开发之window的实现过程的相关资料,需要的朋友可以参考下...2016-05-09