編輯:關於Android編程
咱們今天就來實現一個下拉刷新控件。由於有時候不僅僅是ListView需要下拉刷新,ExpandableListView和GridView也有這個需求,由於ListView,GridView都是AbsListView的子類,ExpandableListView是ListView的子類所以也是AbsListView的子類。所以我的思路是自定義一個對所有AbsListView的子類通用的下拉管理布局,叫PullToRefreshLayout,如果需要GridView,只需要在布局文件裡將ListView換成GridView就行了,ExpandableListView也一樣,不需要再繼承什麼GridView啊ListView啊亂七八糟的。
下面講解PullToRefreshLayout的實現,在貼完整的源碼之前先理解整個類的大概思路:
public class PullToRefreshLayout extends RelativeLayout implements OnTouchListener { // 下拉的距離 public float moveDeltaY = 0; // 是否可以下拉 private boolean canPull = true; private void hideHead() { // 在這裡開始異步隱藏下拉頭,在松手的時候或這刷新完畢的時候隱藏 } public void refreshFinish(int refreshResult) { // 完成刷新操作,顯示刷新結果 } private void changeState(int to) { // 改變當前所處的狀態,有四個狀態:下拉刷新、釋放刷新、正在刷新、刷新完成 } /* * (非 Javadoc)由父控件決定是否分發事件,防止事件沖突 * * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent) */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: /*手指按下的時候,無法判斷是否將要下拉,所以這時候break讓父類把down事件分發給子View 記錄按下的坐標*/ break; case MotionEvent.ACTION_MOVE: /*如果往上滑動且moveDetaY==0則說明不在下拉,break繼續將move事件分發給子View 如果往下拉,則計算下拉的距離moveDeltaY,根據moveDeltaY重新Layout子控件。但是 由於down事件傳到了子View,如果不清除子View的事件,會導致子View誤觸發長按事件和點擊事件。所以在這裡清除子View的事件回調。 下拉超過一定的距離時,改變當前狀態*/ break; case MotionEvent.ACTION_UP: //根據當前狀態執行刷新操作或者hideHead default: break; } // 事件分發交給父類 return super.dispatchTouchEvent(ev); } /* * (非 Javadoc)繪制陰影效果,顏色值可以修改 * * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas) */ @Override protected void dispatchDraw(Canvas canvas) { //在這裡用一個漸變繪制分界線陰影 } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //這個方法就是重新Layout子View了,根據moveDeltaY來定位子View的位置 } @Override public boolean onTouch(View v, MotionEvent event) { //這個是OnTouchListener的方法,只判斷AbsListView的狀態來決定是否canPull,除此之外不做其他處理 } }
可以看到,這裡復寫了ViewGroup的dispatchTouchEvent,這樣就可以掌控事件的分發,如果不了解這個方法可以看一下這篇Android事件分發、View事件Listener全解析。之所以要控制事件分發是因為我們不可能知道手指down在AbsListView上之後將往上滑還是往下拉,所以down事件會分發給AbsListView的,但是在move的時候就需要看情況了,因為我們不想在下拉的同時AbsListView也在滑動,所以在下拉的時候不分發move事件,但這樣問題又來了,前面AbsListView已經接收了down事件,如果這時候不分發move事件給它,它會觸發長按事件或者點擊事件,所以在這裡還需要清除AbsListView消息列表中的callback。
onLayout用於重新布置下拉頭和AbsListView的位置的,這個不難理解。
理解了大概思路之後,看一下PullToRefreshLayout完整的源碼吧~
package com.jingchen.pulltorefresh; import java.lang.reflect.Field; import java.util.Timer; import java.util.TimerTask; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Shader.TileMode; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.widget.AbsListView; import android.widget.RelativeLayout; import android.widget.TextView; /** * 整個下拉刷新就這一個布局,用來管理兩個子控件,其中一個是下拉頭,另一個是包含內容的contentView(可以是AbsListView的任何子類) * * @author 陳靖 */ public class PullToRefreshLayout extends RelativeLayout implements OnTouchListener { public static final String TAG = "PullToRefreshLayout"; // 下拉刷新 public static final int PULL_TO_REFRESH = 0; // 釋放刷新 public static final int RELEASE_TO_REFRESH = 1; // 正在刷新 public static final int REFRESHING = 2; // 刷新完畢 public static final int DONE = 3; // 當前狀態 private int state = PULL_TO_REFRESH; // 刷新回調接口 private OnRefreshListener mListener; // 刷新成功 public static final int REFRESH_SUCCEED = 0; // 刷新失敗 public static final int REFRESH_FAIL = 1; // 下拉頭 private View headView; // 內容 private View contentView; // 按下Y坐標,上一個事件點Y坐標 private float downY, lastY; // 下拉的距離 public float moveDeltaY = 0; // 釋放刷新的距離 private float refreshDist = 200; private Timer timer; private MyTimerTask mTask; // 回滾速度 public float MOVE_SPEED = 8; // 第一次執行布局 private boolean isLayout = false; // 是否可以下拉 private boolean canPull = true; // 在刷新過程中滑動操作 private boolean isTouchInRefreshing = false; // 手指滑動距離與下拉頭的滑動距離比,中間會隨正切函數變化 private float radio = 2; // 下拉箭頭的轉180°動畫 private RotateAnimation rotateAnimation; // 均勻旋轉動畫 private RotateAnimation refreshingAnimation; // 下拉的箭頭 private View pullView; // 正在刷新的圖標 private View refreshingView; // 刷新結果圖標 private View stateImageView; // 刷新結果:成功或失敗 private TextView stateTextView; /** * 執行自動回滾的handler */ Handler updateHandler = new Handler() { @Override public void handleMessage(Message msg) { // 回彈速度隨下拉距離moveDeltaY增大而增大 MOVE_SPEED = (float) (8 + 5 * Math.tan(Math.PI / 2 / getMeasuredHeight() * moveDeltaY)); if (state == REFRESHING && moveDeltaY <= refreshDist && !isTouchInRefreshing) { // 正在刷新,且沒有往上推的話則懸停,顯示"正在刷新..." moveDeltaY = refreshDist; mTask.cancel(); } if (canPull) moveDeltaY -= MOVE_SPEED; if (moveDeltaY <= 0) { // 已完成回彈 moveDeltaY = 0; pullView.clearAnimation(); // 隱藏下拉頭時有可能還在刷新,只有當前狀態不是正在刷新時才改變狀態 if (state != REFRESHING) changeState(PULL_TO_REFRESH); mTask.cancel(); } // 刷新布局,會自動調用onLayout requestLayout(); } }; public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } public PullToRefreshLayout(Context context) { super(context); initView(context); } public PullToRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context); } private void initView(Context context) { timer = new Timer(); mTask = new MyTimerTask(updateHandler); rotateAnimation = (RotateAnimation) AnimationUtils.loadAnimation(context, R.anim.reverse_anim); refreshingAnimation = (RotateAnimation) AnimationUtils.loadAnimation(context, R.anim.rotating); // 添加勻速轉動動畫 LinearInterpolator lir = new LinearInterpolator(); rotateAnimation.setInterpolator(lir); refreshingAnimation.setInterpolator(lir); } private void hideHead() { if (mTask != null) { mTask.cancel(); mTask = null; } mTask = new MyTimerTask(updateHandler); timer.schedule(mTask, 0, 5); } /** * 完成刷新操作,顯示刷新結果 */ public void refreshFinish(int refreshResult) { refreshingView.clearAnimation(); refreshingView.setVisibility(View.GONE); switch (refreshResult) { case REFRESH_SUCCEED: // 刷新成功 stateImageView.setVisibility(View.VISIBLE); stateTextView.setText(R.string.refresh_succeed); stateImageView.setBackgroundResource(R.drawable.refresh_succeed); break; case REFRESH_FAIL: // 刷新失敗 stateImageView.setVisibility(View.VISIBLE); stateTextView.setText(R.string.refresh_fail); stateImageView.setBackgroundResource(R.drawable.refresh_failed); break; default: break; } // 刷新結果停留1秒 new Handler() { @Override public void handleMessage(Message msg) { state = PULL_TO_REFRESH; hideHead(); } }.sendEmptyMessageDelayed(0, 1000); } private void changeState(int to) { state = to; switch (state) { case PULL_TO_REFRESH: // 下拉刷新 stateImageView.setVisibility(View.GONE); stateTextView.setText(R.string.pull_to_refresh); pullView.clearAnimation(); pullView.setVisibility(View.VISIBLE); break; case RELEASE_TO_REFRESH: // 釋放刷新 stateTextView.setText(R.string.release_to_refresh); pullView.startAnimation(rotateAnimation); break; case REFRESHING: // 正在刷新 pullView.clearAnimation(); refreshingView.setVisibility(View.VISIBLE); pullView.setVisibility(View.INVISIBLE); refreshingView.startAnimation(refreshingAnimation); stateTextView.setText(R.string.refreshing); break; default: break; } } /* * (非 Javadoc)由父控件決定是否分發事件,防止事件沖突 * * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent) */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: downY = ev.getY(); lastY = downY; if (mTask != null) { mTask.cancel(); } /* * 觸碰的地方位於下拉頭布局,由於我們沒有對下拉頭做事件響應,這時候它會給咱返回一個false導致接下來的事件不再分發進來。 * 所以我們不能交給父類分發,直接返回true */ if (ev.getY() < moveDeltaY) return true; break; case MotionEvent.ACTION_MOVE: // canPull這個值在底下onTouch中會根據ListView是否滑到頂部來改變,意思是是否可下拉 if (canPull) { // 對實際滑動距離做縮小,造成用力拉的感覺 moveDeltaY = moveDeltaY + (ev.getY() - lastY) / radio; if (moveDeltaY < 0) moveDeltaY = 0; if (moveDeltaY > getMeasuredHeight()) moveDeltaY = getMeasuredHeight(); if (state == REFRESHING) { // 正在刷新的時候觸摸移動 isTouchInRefreshing = true; } } lastY = ev.getY(); // 根據下拉距離改變比例 radio = (float) (2 + 2 * Math.tan(Math.PI / 2 / getMeasuredHeight() * moveDeltaY)); requestLayout(); if (moveDeltaY <= refreshDist && state == RELEASE_TO_REFRESH) { // 如果下拉距離沒達到刷新的距離且當前狀態是釋放刷新,改變狀態為下拉刷新 changeState(PULL_TO_REFRESH); } if (moveDeltaY >= refreshDist && state == PULL_TO_REFRESH) { changeState(RELEASE_TO_REFRESH); } if (moveDeltaY > 8) { // 防止下拉過程中誤觸發長按事件和點擊事件 clearContentViewEvents(); } if (moveDeltaY > 0) { // 正在下拉,不讓子控件捕獲事件 return true; } break; case MotionEvent.ACTION_UP: if (moveDeltaY > refreshDist) // 正在刷新時往下拉釋放後下拉頭不隱藏 isTouchInRefreshing = false; if (state == RELEASE_TO_REFRESH) { changeState(REFRESHING); // 刷新操作 if (mListener != null) mListener.onRefresh(); } else { } hideHead(); default: break; } // 事件分發交給父類 return super.dispatchTouchEvent(ev); } /** * 通過反射修改字段去掉長按事件和點擊事件 */ private void clearContentViewEvents() { try { Field[] fields = AbsListView.class.getDeclaredFields(); for (int i = 0; i < fields.length; i++) if (fields[i].getName().equals("mPendingCheckForLongPress")) { // mPendingCheckForLongPress是AbsListView中的字段,通過反射獲取並從消息列表刪除,去掉長按事件 fields[i].setAccessible(true); contentView.getHandler().removeCallbacks((Runnable) fields[i].get(contentView)); } else if (fields[i].getName().equals("mTouchMode")) { // TOUCH_MODE_REST = -1, 這個可以去除點擊事件 fields[i].setAccessible(true); fields[i].set(contentView, -1); } // 去掉焦點 ((AbsListView) contentView).getSelector().setState(new int[] { 0 }); } catch (Exception e) { Log.d(TAG, "error : " + e.toString()); } } /* * (非 Javadoc)繪制陰影效果,顏色值可以修改 * * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas) */ @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (moveDeltaY == 0) return; RectF rectF = new RectF(0, 0, getMeasuredWidth(), moveDeltaY); Paint paint = new Paint(); paint.setAntiAlias(true); // 陰影的高度為26 LinearGradient linearGradient = new LinearGradient(0, moveDeltaY, 0, moveDeltaY - 26, 0x66000000, 0x00000000, TileMode.CLAMP); paint.setShader(linearGradient); paint.setStyle(Style.FILL); // 在moveDeltaY處往上變淡 canvas.drawRect(rectF, paint); } private void initView() { pullView = headView.findViewById(R.id.pull_icon); stateTextView = (TextView) headView.findViewById(R.id.state_tv); refreshingView = headView.findViewById(R.id.refreshing_icon); stateImageView = headView.findViewById(R.id.state_iv); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!isLayout) { // 這裡是第一次進來的時候做一些初始化 headView = getChildAt(0); contentView = getChildAt(1); // 給AbsListView設置OnTouchListener contentView.setOnTouchListener(this); isLayout = true; initView(); refreshDist = ((ViewGroup) headView).getChildAt(0).getMeasuredHeight(); } if (canPull) { // 改變子控件的布局 headView.layout(0, (int) moveDeltaY - headView.getMeasuredHeight(), headView.getMeasuredWidth(), (int) moveDeltaY); contentView.layout(0, (int) moveDeltaY, contentView.getMeasuredWidth(), (int) moveDeltaY + contentView.getMeasuredHeight()); }else super.onLayout(changed, l, t, r, b); } class MyTimerTask extends TimerTask { Handler handler; public MyTimerTask(Handler handler) { this.handler = handler; } @Override public void run() { handler.sendMessage(handler.obtainMessage()); } } @Override public boolean onTouch(View v, MotionEvent event) { // 第一個item可見且滑動到頂部 AbsListView alv = null; try { alv = (AbsListView) v; } catch (Exception e) { Log.d(TAG, e.getMessage()); return false; } if (alv.getCount() == 0) { // 沒有item的時候也可以下拉刷新 canPull = true; } else if (alv.getFirstVisiblePosition() == 0 && alv.getChildAt(0).getTop() >= 0) { // 滑到AbsListView的頂部了 canPull = true; } else canPull = false; return false; } }
代碼中的注釋已經寫的很清楚了。
既然PullToRefreshLayout已經寫好了,接下來就來使用這個Layout實現下拉刷新~
首先得寫個OnRefreshListener接口來回調刷新操作:
public interface OnRefreshListener { void onRefresh(); }
就一個刷新操作的方法,待會兒讓Activity實現這個接口就可以在Activity中執行刷新操作了。
看一下MainActivity的布局:
PullToRefreshLayout只能包含兩個子控件:refresh_head和content_view。
看一下refresh_head的布局:
可以根據需要修改refresh_head的布局然後在PullToRefreshLayout中處理,但是相關View的id要和PullToRefreshLayout中用到的保持同步!
接下來是MainActivity的代碼:
package com.jingchen.pulltorefresh; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupClickListener; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; /** * 除了下拉刷新,在contenview為ListView的情況下我給ListView增加了FooterView,實現點擊加載更多 * * @author 陳靖 * */ public class MainActivity extends Activity implements OnRefreshListener, OnClickListener { private AbsListView alv; private PullToRefreshLayout refreshLayout; private View loading; private RotateAnimation loadingAnimation; private TextView loadTextView; private MyAdapter adapter; private boolean isLoading = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { alv = (AbsListView) findViewById(R.id.content_view); refreshLayout = (PullToRefreshLayout) findViewById(R.id.refresh_view); refreshLayout.setOnRefreshListener(this); initListView(); loadingAnimation = (RotateAnimation) AnimationUtils.loadAnimation(this, R.anim.rotating); // 添加勻速轉動動畫 LinearInterpolator lir = new LinearInterpolator(); loadingAnimation.setInterpolator(lir); } /** * ListView初始化方法 */ private void initListView() { Listitems = new ArrayList (); for (int i = 0; i < 30; i++) { items.add("這裡是item " + i); } // 添加head View headView = getLayoutInflater().inflate(R.layout.listview_head, null); ((ListView) alv).addHeaderView(headView, null, false); // 添加footer View footerView = getLayoutInflater().inflate(R.layout.load_more, null); loading = footerView.findViewById(R.id.loading_icon); loadTextView = (TextView) footerView.findViewById(R.id.loadmore_tv); ((ListView) alv).addFooterView(footerView, null, false); footerView.setOnClickListener(this); adapter = new MyAdapter(this, items); alv.setAdapter(adapter); alv.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show(); return true; } }); alv.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, " Click on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show(); } }); } /** * GridView初始化方法 */ private void initGridView() { List items = new ArrayList (); for (int i = 0; i < 30; i++) { items.add("這裡是item " + i); } adapter = new MyAdapter(this, items); alv.setAdapter(adapter); alv.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show(); return true; } }); alv.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, " Click on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show(); } }); } /** * ExpandableListView初始化方法 */ private void initExpandableListView() { ((ExpandableListView) alv).setAdapter(new ExpandableListAdapter(this)); ((ExpandableListView) alv).setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { Toast.makeText(MainActivity.this, " Click on group " + groupPosition + " item " + childPosition, Toast.LENGTH_SHORT).show(); return true; } }); ((ExpandableListView) alv).setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show(); return true; } }); ((ExpandableListView) alv).setOnGroupClickListener(new OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { if (parent.isGroupExpanded(groupPosition)) { // 如果展開則關閉 parent.collapseGroup(groupPosition); } else { // 如果關閉則打開,注意這裡是手動打開不要默認滾動否則會有bug parent.expandGroup(groupPosition); } return true; } }); } @Override public void onRefresh() { // 下拉刷新操作 new Handler() { @Override public void handleMessage(Message msg) { refreshLayout.refreshFinish(PullToRefreshLayout.REFRESH_SUCCEED); } }.sendEmptyMessageDelayed(0, 5000); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.loadmore_layout: if (!isLoading) { loading.setVisibility(View.VISIBLE); loading.startAnimation(loadingAnimation); loadTextView.setText(R.string.loading); isLoading = true; } break; default: break; } } class ExpandableListAdapter extends BaseExpandableListAdapter { private String[] groupsStrings;// = new String[] { "這裡是group 0", // "這裡是group 1", "這裡是group 2" }; private String[][] groupItems; private Context context; public ExpandableListAdapter(Context context) { this.context = context; groupsStrings = new String[8]; for (int i = 0; i < groupsStrings.length; i++) { groupsStrings[i] = new String("這裡是group " + i); } groupItems = new String[8][8]; for (int i = 0; i < groupItems.length; i++) for (int j = 0; j < groupItems[i].length; j++) { groupItems[i][j] = new String("這裡是group " + i + "裡的item " + j); } } @Override public int getGroupCount() { return groupsStrings.length; } @Override public int getChildrenCount(int groupPosition) { return groupItems[groupPosition].length; } @Override public Object getGroup(int groupPosition) { return groupsStrings[groupPosition]; } @Override public Object getChild(int groupPosition, int childPosition) { return groupItems[groupPosition][childPosition]; } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public boolean hasStableIds() { return true; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { View view = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null); TextView tv = (TextView) view.findViewById(R.id.name_tv); tv.setText(groupsStrings[groupPosition]); return view; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { View view = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null); TextView tv = (TextView) view.findViewById(R.id.name_tv); tv.setText(groupItems[groupPosition][childPosition]); return view; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } } }
在MainActivity中判斷contentView是ListView的話給ListView添加了FooterView實現點擊加載更多的功能。這只是一個演示PullToRefreshLayout使用的demo,可以參照一下修改。我已經在裡面寫了ListView,GridView和ExpandableListView的初始化方法,根據自己使用的是哪個來調用吧。那麼這是ListView的下拉刷新和加載更多。如果我要GridView也有下拉刷新功能呢?那就把MainActivity的布局換成這樣:
怎麼樣?很簡單吧?簡單易用,不用再去繼承修改了。
希望本文所述對大家學習Android下拉刷新控件有所幫助。
以前看別人的程序的drawable文件夾裡有xml資源,說實話第一次見到這樣的xml圖像資源時,我真心不知道是干什麼的。抽空學習了一下圖像資源,才了解了這類圖像資源的妙用
干貨來啦~! 想在聊天中發 小視頻?gif 動圖? 發紅包? 發 自定義表情? 沒有問題!在融雲統統都可以實現! 以上不管是 小視頻 還是 gif 還是 紅包 或者是 自
一,IntelliJ 代碼檢查IntelliJ IDEA的具有強大,快速,靈活的靜態代碼分析。它可以檢測編譯器和運行時錯誤,提出改進和完善,甚至在編譯之前。代碼檢查基礎(
=============================== 准備 1,導入銀聯支付libs:UPPayAssistEx.jar;UPPayPluginEx.jar;