你的位置:首页 > 操作系统

[操作系统]Android之自定义控件


实现效果:

 

 

图片素材:        

 

--> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.

 1 <resources> 2   <string name="app_name">PullToRefreshTest</string> 3   <string name="pull_to_refresh">下拉可以刷新</string> 4   <string name="release_to_refresh">释放立即刷新</string> 5   <string name="refreshing">正在刷新...</string> 6   <string name="not_updated_yet">暂未更新过</string> 7   <string name="updated_at">上次更新于%1$s前</string> 8   <string name="updated_just_now">刚刚更新</string> 9   <string name="time_error">时间有问题</string>10 </resources>

strings
 1 <??> 2 <RelativeLayout ="http://schemas.android.com/apk/res/android" 3   android:id="@+id/pull_to_refresh_head" 4   android:layout_width="match_parent" 5   android:layout_height="60dp"> 6  7   <LinearLayout 8     android:layout_width="200dp" 9     android:layout_height="60dp"10     android:layout_centerInParent="true"11     android:orientation="horizontal">12 13     <RelativeLayout14       android:layout_width="0dp"15       android:layout_height="60dp"16       android:layout_weight="3">17 18       <ImageView19         android:id="@+id/arrow"20         android:layout_width="wrap_content"21         android:layout_height="wrap_content"22         android:layout_centerInParent="true"23         android:src="@mipmap/indicator_arrow" />24 25       <ProgressBar26         android:id="@+id/progress_bar"27         android:layout_width="30dp"28         android:layout_height="30dp"29         android:layout_centerInParent="true"30         android:visibility="gone" />31     </RelativeLayout>32 33     <LinearLayout34       android:layout_width="0dp"35       android:layout_height="60dp"36       android:layout_weight="12"37       android:orientation="vertical">38 39       <TextView40         android:id="@+id/description"41         android:layout_width="match_parent"42         android:layout_height="0dp"43         android:layout_weight="1"44         android:gravity="center_horizontal|bottom"45         android:text="@string/pull_to_refresh" />46 47       <TextView48         android:id="@+id/updated_at"49         android:layout_width="match_parent"50         android:layout_height="0dp"51         android:layout_weight="1"52         android:gravity="center_horizontal|top"53         android:text="@string/updated_at" />54     </LinearLayout>55   </LinearLayout>56 57 </RelativeLayout>

pull_to_refresh

 

--> 然后, 也是主要的, 自定义下拉刷新的 View (包含下拉刷新所有操作) RefreshView.java:

 1 package com.dragon.android.tofreshlayout; 2  3 import android.content.Context; 4 import android.content.SharedPreferences; 5 import android.os.AsyncTask; 6 import android.os.SystemClock; 7 import android.preference.PreferenceManager; 8 import android.util.AttributeSet; 9 import android.view.LayoutInflater; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.view.ViewConfiguration; 13 import android.view.animation.RotateAnimation; 14 import android.widget.ImageView; 15 import android.widget.LinearLayout; 16 import android.widget.ListView; 17 import android.widget.ProgressBar; 18 import android.widget.TextView; 19  20 public class RefreshView extends LinearLayout implements View.OnTouchListener { 21  22   private static final String TAG = RefreshView.class.getSimpleName(); 23  24   public enum PULL_STATUS { 25     STATUS_PULL_TO_REFRESH(0), // 下拉状态 26     STATUS_RELEASE_TO_REFRESH(1), // 释放立即刷新状态 27     STATUS_REFRESHING(2), // 正在刷新状态 28     STATUS_REFRESH_FINISHED(3); // 刷新完成或未刷新状态 29  30     private int status; // 状态 31  32     PULL_STATUS(int value) { 33       this.status = value; 34     } 35  36     public int getValue() { 37       return this.status; 38     } 39   } 40  41   // 下拉头部回滚的速度 42   public static final int SCROLL_SPEED = -20; 43   // 一分钟的毫秒值,用于判断上次的更新时间 44   public static final long ONE_MINUTE = 60 * 1000; 45   // 一小时的毫秒值,用于判断上次的更新时间 46   public static final long ONE_HOUR = 60 * ONE_MINUTE; 47   // 一天的毫秒值,用于判断上次的更新时间 48   public static final long ONE_DAY = 24 * ONE_HOUR; 49   // 一月的毫秒值,用于判断上次的更新时间 50   public static final long ONE_MONTH = 30 * ONE_DAY; 51   // 一年的毫秒值,用于判断上次的更新时间 52   public static final long ONE_YEAR = 12 * ONE_MONTH; 53   // 上次更新时间的字符串常量,用于作为 SharedPreferences 的键值 54   private static final String UPDATED_AT = "updated_at"; 55  56   // 下拉刷新的回调接口 57   private PullToRefreshListener mListener; 58  59   private SharedPreferences preferences; // 用于存储上次更新时间 60   private View header; // 下拉头的View 61   private ListView listView; // 需要去下拉刷新的ListView 62  63   private ProgressBar progressBar; // 刷新时显示的进度条 64   private ImageView arrow; // 指示下拉和释放的箭头 65   private TextView description; // 指示下拉和释放的文字描述 66   private TextView updateAt; // 上次更新时间的文字描述 67  68   private MarginLayoutParams headerLayoutParams; // 下拉头的布局参数 69   private long lastUpdateTime; // 上次更新时间的毫秒值 70  71   // 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分 72   private int mId = -1; 73  74   private int hideHeaderHeight; // 下拉头的高度 75  76   /** 77    * 当前处理什么状态,可选值有 STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED 78   */ 79   private PULL_STATUS currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED; 80  81   // 记录上一次的状态是什么,避免进行重复操作 82   private PULL_STATUS lastStatus = currentStatus; 83  84   private float yDown; // 手指按下时的屏幕纵坐标 85  86   private int touchSlop; // 在被判定为滚动之前用户手指可以移动的最大值。 87  88   private boolean loadOnce; // 是否已加载过一次layout,这里onLayout中的初始化只需加载一次 89  90   private boolean ableToPull; // 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉 91  92   /** 93    * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局 94   */ 95   public RefreshView(Context context, AttributeSet attrs) { 96     super(context, attrs); 97  98     preferences = PreferenceManager.getDefaultSharedPreferences(context); 99     header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);100     progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);101     arrow = (ImageView) header.findViewById(R.id.arrow);102     description = (TextView) header.findViewById(R.id.description);103     updateAt = (TextView) header.findViewById(R.id.updated_at);104     touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();105 106     refreshUpdatedAtValue();107     setOrientation(VERTICAL);108     addView(header, 0);109 110     //Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(0));111     //Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(1));112 113 //    listView = (ListView) getChildAt(1);114 //    listView.setOnTouchListener(this);115   }116 117   /**118    * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给 ListView 注册 touch 事件119   */120   @Override121   protected void onLayout(boolean changed, int l, int t, int r, int b) {122     super.onLayout(changed, l, t, r, b);123     if (changed && !loadOnce) {124       hideHeaderHeight = -header.getHeight();125 126       headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();127       headerLayoutParams.topMargin = hideHeaderHeight;128       listView = (ListView) getChildAt(1);129       //Log.d(TAG, "onLayout() getChildAt(0): " + getChildAt(0));130       //Log.d(TAG, "onLayout() listView: " + listView);131       listView.setOnTouchListener(this);132       loadOnce = true;133     }134   }135 136   /**137    * 当 ListView 被触摸时调用,其中处理了各种下拉刷新的具体逻辑138   */139   @Override140   public boolean onTouch(View v, MotionEvent event) {141     setCanAbleToPull(event); // 判断是否可以下拉142     if (ableToPull) {143       switch (event.getAction()) {144         case MotionEvent.ACTION_DOWN:145           yDown = event.getRawY();146           break;147         case MotionEvent.ACTION_MOVE:148           // 获取移动中的 Y 轴的位置149           float yMove = event.getRawY();150           // 获取从按下到移动过程中移动的距离151           int distance = (int) (yMove - yDown);152 153           // 如果手指是上滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件154           if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {155             return false;156           }157           if (distance < touchSlop) {158             return false;159           }160           // 判断是否已经在刷新状态161           if (currentStatus != PULL_STATUS.STATUS_REFRESHING) {162             // 判断设置的 topMargin 是否 > 0, 默认初始设置为 -header.getHeight()163             if (headerLayoutParams.topMargin > 0) {164               currentStatus = PULL_STATUS.STATUS_RELEASE_TO_REFRESH;165             } else {166               // 否则状态为下拉中的状态167               currentStatus = PULL_STATUS.STATUS_PULL_TO_REFRESH;168             }169             // 通过偏移下拉头的 topMargin 值,来实现下拉效果170             headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;171             header.setLayoutParams(headerLayoutParams);172           }173           break;174         case MotionEvent.ACTION_UP:175         default:176           if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {177             // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务178             new RefreshingTask().execute();179           } else if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {180             // 松手时如果是下拉状态,就去调用隐藏下拉头的任务181             new HideHeaderTask().execute();182           }183           break;184       }185       // 时刻记得更新下拉头中的信息186       if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH187           || currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {188         updateHeaderView();189         // 当前正处于下拉或释放状态,要让 ListView 失去焦点,否则被点击的那一项会一直处于选中状态190         listView.setPressed(false);191         listView.setFocusable(false);192         listView.setFocusableInTouchMode(false);193         lastStatus = currentStatus;194         // 当前正处于下拉或释放状态,通过返回 true 屏蔽掉 ListView 的滚动事件195         return true;196       }197     }198     return false;199   }200 201   /**202    * 给下拉刷新控件注册一个监听器203    *204    * @param listener 监听器的实现205    * @param id    为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,不同界面在注册下拉刷新监听器时一定要传入不同的 id206   */207   public void setOnRefreshListener(PullToRefreshListener listener, int id) {208     mListener = listener;209     mId = id;210   }211 212   /**213    * 当所有的刷新逻辑完成后,记录调用一下,否则你的 ListView 将一直处于正在刷新状态214   */215   public void finishRefreshing() {216     currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;217     preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();218     new HideHeaderTask().execute();219   }220 221   /**222    * 根据当前 ListView 的滚动状态来设定 {@link #ableToPull}223    * 的值,每次都需要在 onTouch 中第一个执行,这样可以判断出当前应该是滚动 ListView,还是应该进行下拉224   */225   private void setCanAbleToPull(MotionEvent event) {226     View firstChild = listView.getChildAt(0);227     if (firstChild != null) {228       // 获取 ListView 中第一个Item的位置229       int firstVisiblePos = listView.getFirstVisiblePosition();230       // 判断第一个子控件的 Top 是否和第一个 Item 位置相等231       if (firstVisiblePos == 0 && firstChild.getTop() == 0) {232         if (!ableToPull) {233           // getRawY() 获得的是相对屏幕 Y 方向的位置234           yDown = event.getRawY();235         }236         // 如果首个元素的上边缘,距离父布局值为 0,就说明 ListView 滚动到了最顶部,此时应该允许下拉刷新237         ableToPull = true;238       } else {239         if (headerLayoutParams.topMargin != hideHeaderHeight) {240           headerLayoutParams.topMargin = hideHeaderHeight;241           header.setLayoutParams(headerLayoutParams);242         }243         ableToPull = false;244       }245     } else {246       // 如果 ListView 中没有元素,也应该允许下拉刷新247       ableToPull = true;248     }249   }250 251   /**252    * 更新下拉头中的信息253   */254   private void updateHeaderView() {255     if (lastStatus != currentStatus) {256       if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {257         description.setText(getResources().getString(R.string.pull_to_refresh));258         arrow.setVisibility(View.VISIBLE);259         progressBar.setVisibility(View.GONE);260         rotateArrow();261       } else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {262         description.setText(getResources().getString(R.string.release_to_refresh));263         arrow.setVisibility(View.VISIBLE);264         progressBar.setVisibility(View.GONE);265         rotateArrow();266       } else if (currentStatus == PULL_STATUS.STATUS_REFRESHING) {267         description.setText(getResources().getString(R.string.refreshing));268         progressBar.setVisibility(View.VISIBLE);269         arrow.clearAnimation();270         arrow.setVisibility(View.GONE);271       }272       refreshUpdatedAtValue();273     }274   }275 276   /**277    * 根据当前的状态来旋转箭头278   */279   private void rotateArrow() {280     float pivotX = arrow.getWidth() / 2f;281     float pivotY = arrow.getHeight() / 2f;282     float fromDegrees = 0f;283     float toDegrees = 0f;284     if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {285       fromDegrees = 180f;286       toDegrees = 360f;287     } else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {288       fromDegrees = 0f;289       toDegrees = 180f;290     }291     RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);292     animation.setDuration(100);293     animation.setFillAfter(true);294     arrow.startAnimation(animation);295   }296 297   /**298    * 刷新下拉头中上次更新时间的文字描述299   */300   private void refreshUpdatedAtValue() {301     lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);302     long currentTime = System.currentTimeMillis();303     long timePassed = currentTime - lastUpdateTime;304     long timeIntoFormat;305     String updateAtValue;306     if (lastUpdateTime == -1) {307       updateAtValue = getResources().getString(R.string.not_updated_yet);308     } else if (timePassed < 0) {309       updateAtValue = getResources().getString(R.string.time_error);310     } else if (timePassed < ONE_MINUTE) {311       updateAtValue = getResources().getString(R.string.updated_just_now);312     } else if (timePassed < ONE_HOUR) {313       timeIntoFormat = timePassed / ONE_MINUTE;314       String value = timeIntoFormat + "分钟";315       updateAtValue = String.format(getResources().getString(R.string.updated_at), value);316     } else if (timePassed < ONE_DAY) {317       timeIntoFormat = timePassed / ONE_HOUR;318       String value = timeIntoFormat + "小时";319       updateAtValue = String.format(getResources().getString(R.string.updated_at), value);320     } else if (timePassed < ONE_MONTH) {321       timeIntoFormat = timePassed / ONE_DAY;322       String value = timeIntoFormat + "天";323       updateAtValue = String.format(getResources().getString(R.string.updated_at), value);324     } else if (timePassed < ONE_YEAR) {325       timeIntoFormat = timePassed / ONE_MONTH;326       String value = timeIntoFormat + "个月";327       updateAtValue = String.format(getResources().getString(R.string.updated_at), value);328     } else {329       timeIntoFormat = timePassed / ONE_YEAR;330       String value = timeIntoFormat + "年";331       updateAtValue = String.format(getResources().getString(R.string.updated_at), value);332     }333     updateAt.setText(updateAtValue);334   }335 336   /**337    * 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器338   */339   class RefreshingTask extends AsyncTask<Void, Integer, Void> {340 341     @Override342     protected Void doInBackground(Void... params) {343       int topMargin = headerLayoutParams.topMargin;344       while (true) {345         topMargin = topMargin + SCROLL_SPEED;346         if (topMargin <= 0) {347           topMargin = 0;348           break;349         }350         publishProgress(topMargin);351         SystemClock.sleep(10);352       }353       currentStatus = PULL_STATUS.STATUS_REFRESHING;354       publishProgress(0);355       if (mListener != null) {356         mListener.onRefresh();357       }358       return null;359     }360 361     @Override362     protected void onProgressUpdate(Integer... topMargin) {363       updateHeaderView();364       headerLayoutParams.topMargin = topMargin[0];365       header.setLayoutParams(headerLayoutParams);366     }367 368   }369 370   /**371    * 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏372   */373   class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {374 375     @Override376     protected Integer doInBackground(Void... params) {377       int topMargin = headerLayoutParams.topMargin;378       while (true) {379         topMargin = topMargin + SCROLL_SPEED;380         if (topMargin <= hideHeaderHeight) {381           topMargin = hideHeaderHeight;382           break;383         }384         publishProgress(topMargin);385         SystemClock.sleep(10);386       }387       return topMargin;388     }389 390     @Override391     protected void onProgressUpdate(Integer ... topMargin) {392       headerLayoutParams.topMargin = topMargin[0];393       header.setLayoutParams(headerLayoutParams);394     }395 396     @Override397     protected void onPostExecute(Integer topMargin) {398       headerLayoutParams.topMargin = topMargin;399       header.setLayoutParams(headerLayoutParams);400       currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;401     }402   }403 404   /**405    * 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调406   */407   public interface PullToRefreshListener {408     // 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 可以不必另开线程来进行耗时操作409     void onRefresh();410   }411 }

 

--> 第三步, 写主布局:

 1 <??> 2 <RelativeLayout ="http://schemas.android.com/apk/res/android" 3   ="http://schemas.android.com/tools" 4   android:layout_width="match_parent" 5   android:layout_height="match_parent" 6   tools:context=".MainActivity" > 7  8   <com.dragon.android.tofreshlayout.RefreshView 9     android:id="@+id/refreshable_view"10     android:layout_width="match_parent"11     android:layout_height="match_parent" >12 13     <ListView14       android:id="@+id/list_view"15       android:layout_width="match_parent"16       android:layout_height="match_parent" >17     </ListView>18 19   </com.dragon.android.tofreshlayout.RefreshView>20 21 </RelativeLayout>

activity_main

 

--> 最后, Java 代码添加 ListView 的数据:

package com.dragon.android.tofreshlayout;import android.os.Bundle;import android.os.SystemClock;import android.support.v7.app.AppCompatActivity;import android.webkit.WebView;import android.widget.ArrayAdapter;import android.widget.ListView;public class MainActivity extends AppCompatActivity {  RefreshView refreshableView;  ListView listView;  ArrayAdapter<String> adapter;  private WebView webView;  private static int NUM = 30;  String[] items = new String[NUM];  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    getSupportActionBar().hide();    for (int i = 0; i < items.length; i++) {      items[i] = "列表项" + i;    }    refreshableView = (RefreshView) findViewById(R.id.refreshable_view);    listView = (ListView) findViewById(R.id.list_view);    adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);    listView.setAdapter(adapter);    refreshableView.setOnRefreshListener(new RefreshView.PullToRefreshListener() {      @Override      public void onRefresh() {        SystemClock.sleep(3000);        refreshableView.finishRefreshing();      }    }, 0);  }}

View Code

 

程序 Demo: 链接:http://pan.baidu.com/s/1ge6Llw3 密码:skna

***************其实还应该再封装的...*****************