XListview是一个非常受欢迎的下拉刷新控件,但是已经停止维护了。之前写过一篇XListview的使用介绍,用起来非常简单,这两天放假无聊,研究了下XListview的实现原理,学到了很多,今天分享给大家。

提前声明,为了让代码更好的理解,我对代码进行了部分删减和重构,如果大家想看原版代码,请去github自行下载。

Xlistview项目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分别是XListView主体、header、footer的实现。下面我们分开来介绍。

下面是修改之后的XListViewHeader代码

public class XListViewHeader extends LinearLayout {  

    private static final String HINT_NORMAL = "下拉刷新";
private static final String HINT_READY = "松开刷新数据";
private static final String HINT_LOADING = "正在加载..."; // 正常状态
public final static int STATE_NORMAL = ;
// 准备刷新状态,也就是箭头方向发生改变之后的状态
public final static int STATE_READY = ;
// 刷新状态,箭头变成了progressBar
public final static int STATE_REFRESHING = ;
// 布局容器,也就是根布局
private LinearLayout container;
// 箭头图片
private ImageView mArrowImageView;
// 刷新状态显示
private ProgressBar mProgressBar;
// 说明文本
private TextView mHintTextView;
// 记录当前的状态
private int mState;
// 用于改变箭头的方向的动画
private Animation mRotateUpAnim;
private Animation mRotateDownAnim;
// 动画持续时间
private final int ROTATE_ANIM_DURATION = ; public XListViewHeader(Context context) {
super(context);
initView(context);
} public XListViewHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
} private void initView(Context context) {
mState = STATE_NORMAL;
// 初始情况下,设置下拉刷新view高度为0
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, );
container = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.xlistview_header, null);
addView(container, lp);
// 初始化控件
mArrowImageView = (ImageView) findViewById(R.id.xlistview_header_arrow);
mHintTextView = (TextView) findViewById(R.id.xlistview_header_hint_textview);
mProgressBar = (ProgressBar) findViewById(R.id.xlistview_header_progressbar);
// 初始化动画
mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateUpAnim.setFillAfter(true);
mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateDownAnim.setFillAfter(true);
} // 设置header的状态
public void setState(int state) {
if (state == mState)
return; // 显示进度
if (state == STATE_REFRESHING) {
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
} else {
// 显示箭头
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
} switch (state) {
case STATE_NORMAL:
if (mState == STATE_READY) {
mArrowImageView.startAnimation(mRotateDownAnim);
}
if (mState == STATE_REFRESHING) {
mArrowImageView.clearAnimation();
}
mHintTextView.setText(HINT_NORMAL);
break;
case STATE_READY:
if (mState != STATE_READY) {
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mHintTextView.setText(HINT_READY);
}
break;
case STATE_REFRESHING:
mHintTextView.setText(HINT_LOADING);
break;
} mState = state;
} public void setVisiableHeight(int height) {
if (height < )
height = ;
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) container
.getLayoutParams();
lp.height = height;
container.setLayoutParams(lp);
} public int getVisiableHeight() {
return container.getHeight();
} public void show() {
container.setVisibility(View.VISIBLE);
} public void hide() {
container.setVisibility(View.INVISIBLE);
} }

XListViewHeader继承自linearLayout,用来实现下拉刷新时的界面展示,可以分为三种状态:正常、准备刷新、正在加载。

在Linearlayout布局里面,主要有指示箭头、说明文本、圆形加载条三个控件。在构造函数中,调用了initView()进行控件的初始化操作。在添加布局文件的时候,指定高度为0,这是为了隐藏header,然后初始化动画,是为了完成箭头的旋转动作。

setState()是设置header的状态,因为header需要根据不同的状态,完成控件隐藏、显示、改变文字等操作,这个方法主要是在XListView里面调用。除此之外,还有setVisiableHeight()和getVisiableHeight(),这两个方法是为了设置和获取Header中根布局文件的高度属性,从而完成拉伸和收缩的效果,而show()和hide()则显然就是完成显示和隐藏的效果。

下面是Header的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom" > <RelativeLayout
android:id="@+id/xlistview_header_content"
android:layout_width="match_parent"
android:layout_height="60dp"
tools:ignore="UselessParent" > <TextView
android:id="@+id/xlistview_header_hint_textview"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text="正在加载"
android:textColor="@android:color/black"
android:textSize="14sp" /> <ImageView
android:id="@+id/xlistview_header_arrow"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/xlistview_header_hint_textview"
android:src="@drawable/xlistview_arrow" /> <ProgressBar
android:id="@+id/xlistview_header_progressbar"
style="@style/progressbar_style"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/xlistview_header_hint_textview"
android:visibility="invisible" />
</RelativeLayout> </LinearLayout>

说完了Header,我们再看看Footer。Footer是为了完成加载更多功能时候的界面展示,基本思路和Header是一样的,下面是Footer的代码

public class XListViewFooter extends LinearLayout {  

    // 正常状态
public final static int STATE_NORMAL = ;
// 准备状态
public final static int STATE_READY = ;
// 加载状态
public final static int STATE_LOADING = ; private View mContentView;
private View mProgressBar;
private TextView mHintView; public XListViewFooter(Context context) {
super(context);
initView(context);
} public XListViewFooter(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
} private void initView(Context context) { LinearLayout moreView = (LinearLayout) LayoutInflater.from(context)
.inflate(R.layout.xlistview_footer, null);
addView(moreView);
moreView.setLayoutParams(new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); mContentView = moreView.findViewById(R.id.xlistview_footer_content);
mProgressBar = moreView.findViewById(R.id.xlistview_footer_progressbar);
mHintView = (TextView) moreView
.findViewById(R.id.xlistview_footer_hint_textview);
} /**
* 设置当前的状态
*
* @param state
*/
public void setState(int state) { mProgressBar.setVisibility(View.INVISIBLE);
mHintView.setVisibility(View.INVISIBLE); switch (state) {
case STATE_READY:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_ready);
break; case STATE_NORMAL:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_normal);
break; case STATE_LOADING:
mProgressBar.setVisibility(View.VISIBLE);
break; } } public void setBottomMargin(int height) {
if (height > ) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.bottomMargin = height;
mContentView.setLayoutParams(lp);
}
} public int getBottomMargin() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
return lp.bottomMargin;
} public void hide() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.height = ;
mContentView.setLayoutParams(lp);
} public void show() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.height = LayoutParams.WRAP_CONTENT;
mContentView.setLayoutParams(lp);
} }

从上面的代码里面,我们可以看出,footer和header的思路是一样的,只不过,footer的拉伸和显示效果不是通过高度来模拟的,而是通过设置BottomMargin来完成的。

下面是Footer的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content" > <RelativeLayout
android:id="@+id/xlistview_footer_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dp"
tools:ignore="UselessParent" > <ProgressBar
android:id="@+id/xlistview_footer_progressbar"
style="@style/progressbar_style"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerInParent="true"
android:visibility="invisible" /> <TextView
android:id="@+id/xlistview_footer_hint_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/xlistview_footer_hint_normal"
android:textColor="@android:color/black"
android:textSize="14sp" />
</RelativeLayout> </LinearLayout>

在了解了Header和footer之后,我们就要介绍最核心的XListView的代码实现了。

在介绍代码实现之前,我先介绍一下XListView的实现原理。

首先,一旦使用XListView,Footer和Header就已经添加到我们的ListView上面了,XListView就是通过继承ListView,然后处理了屏幕点击事件和控制滑动实现效果的。所以,如果我们的Adapter中getCount()返回的值是20,那么其实XListView里面是有20+2个item的,这个数量即使我们关闭了XListView的刷新和加载功能,也是不会变化的。Header和Footer通过addHeaderView和addFooterView添加上去之后,如果想实现下拉刷新和上拉加载功能,那么就必须有拉伸效果,所以就像上面的那样,Header是通过设置height,Footer是通过设置BottomMargin来模拟拉伸效果。那么回弹效果呢?仅仅通过设置高度或者是间隔是达不到模拟回弹效果的,因此,就需要用Scroller来实现模拟回弹效果。在说明原理之后,我们开始介绍XListView的核心实现原理。

再次提示,下面的代码经过我重构了,只是为了看起来更好的理解。

public class XListView extends ListView {  

    private final static int SCROLLBACK_HEADER = ;
private final static int SCROLLBACK_FOOTER = ;
// 滑动时长
private final static int SCROLL_DURATION = ;
// 加载更多的距离
private final static int PULL_LOAD_MORE_DELTA = ;
// 滑动比例
private final static float OFFSET_RADIO = 2f;
// 记录按下点的y坐标
private float lastY;
// 用来回滚
private Scroller scroller;
private IXListViewListener mListViewListener;
private XListViewHeader headerView;
private RelativeLayout headerViewContent;
// header的高度
private int headerHeight;
// 是否能够刷新
private boolean enableRefresh = true;
// 是否正在刷新
private boolean isRefreashing = false;
// footer
private XListViewFooter footerView;
// 是否可以加载更多
private boolean enableLoadMore;
// 是否正在加载
private boolean isLoadingMore;
// 是否footer准备状态
private boolean isFooterAdd = false;
// total list items, used to detect is at the bottom of listview.
private int totalItemCount;
// 记录是从header还是footer返回
private int mScrollBack; private static final String TAG = "XListView"; public XListView(Context context) {
super(context);
initView(context);
} public XListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
} public XListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
} private void initView(Context context) { scroller = new Scroller(context, new DecelerateInterpolator()); headerView = new XListViewHeader(context);
footerView = new XListViewFooter(context); headerViewContent = (RelativeLayout) headerView
.findViewById(R.id.xlistview_header_content);
headerView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
headerHeight = headerViewContent.getHeight();
getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
});
addHeaderView(headerView); } @Override
public void setAdapter(ListAdapter adapter) {
// 确保footer最后添加并且只添加一次
if (isFooterAdd == false) {
isFooterAdd = true;
addFooterView(footerView);
}
super.setAdapter(adapter); } @Override
public boolean onTouchEvent(MotionEvent ev) { totalItemCount = getAdapter().getCount();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录按下的坐标
lastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 计算移动距离
float deltaY = ev.getRawY() - lastY;
lastY = ev.getRawY();
// 是第一项并且标题已经显示或者是在下拉
if (getFirstVisiblePosition() ==
&& (headerView.getVisiableHeight() > || deltaY > )) {
updateHeaderHeight(deltaY / OFFSET_RADIO);
} else if (getLastVisiblePosition() == totalItemCount -
&& (footerView.getBottomMargin() > || deltaY < )) {
updateFooterHeight(-deltaY / OFFSET_RADIO);
}
break; case MotionEvent.ACTION_UP: if (getFirstVisiblePosition() == ) {
if (enableRefresh
&& headerView.getVisiableHeight() > headerHeight) {
isRefreashing = true;
headerView.setState(XListViewHeader.STATE_REFRESHING);
if (mListViewListener != null) {
mListViewListener.onRefresh();
}
}
resetHeaderHeight();
} else if (getLastVisiblePosition() == totalItemCount - ) {
if (enableLoadMore
&& footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
startLoadMore();
}
resetFooterHeight();
}
break;
}
return super.onTouchEvent(ev);
} @Override
public void computeScroll() { // 松手之后调用
if (scroller.computeScrollOffset()) { if (mScrollBack == SCROLLBACK_HEADER) {
headerView.setVisiableHeight(scroller.getCurrY());
} else {
footerView.setBottomMargin(scroller.getCurrY());
}
postInvalidate();
}
super.computeScroll(); } public void setPullRefreshEnable(boolean enable) {
enableRefresh = enable; if (!enableRefresh) {
headerView.hide();
} else {
headerView.show();
}
} public void setPullLoadEnable(boolean enable) {
enableLoadMore = enable;
if (!enableLoadMore) {
footerView.hide();
footerView.setOnClickListener(null);
} else {
isLoadingMore = false;
footerView.show();
footerView.setState(XListViewFooter.STATE_NORMAL);
footerView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startLoadMore();
}
});
}
} public void stopRefresh() {
if (isRefreashing == true) {
isRefreashing = false;
resetHeaderHeight();
}
} public void stopLoadMore() {
if (isLoadingMore == true) {
isLoadingMore = false;
footerView.setState(XListViewFooter.STATE_NORMAL);
}
} private void updateHeaderHeight(float delta) {
headerView.setVisiableHeight((int) delta
+ headerView.getVisiableHeight());
// 未处于刷新状态,更新箭头
if (enableRefresh && !isRefreashing) {
if (headerView.getVisiableHeight() > headerHeight) {
headerView.setState(XListViewHeader.STATE_READY);
} else {
headerView.setState(XListViewHeader.STATE_NORMAL);
}
} } private void resetHeaderHeight() {
// 当前的可见高度
int height = headerView.getVisiableHeight();
// 如果正在刷新并且高度没有完全展示
if ((isRefreashing && height <= headerHeight) || (height == )) {
return;
}
// 默认会回滚到header的位置
int finalHeight = ;
// 如果是正在刷新状态,则回滚到header的高度
if (isRefreashing && height > headerHeight) {
finalHeight = headerHeight;
}
mScrollBack = SCROLLBACK_HEADER;
// 回滚到指定位置
scroller.startScroll(, height, , finalHeight - height,
SCROLL_DURATION);
// 触发computeScroll
invalidate();
} private void updateFooterHeight(float delta) {
int height = footerView.getBottomMargin() + (int) delta;
if (enableLoadMore && !isLoadingMore) {
if (height > PULL_LOAD_MORE_DELTA) {
footerView.setState(XListViewFooter.STATE_READY);
} else {
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
footerView.setBottomMargin(height); } private void resetFooterHeight() {
int bottomMargin = footerView.getBottomMargin();
if (bottomMargin > ) {
mScrollBack = SCROLLBACK_FOOTER;
scroller.startScroll(, bottomMargin, , -bottomMargin,
SCROLL_DURATION);
invalidate();
}
} private void startLoadMore() {
isLoadingMore = true;
footerView.setState(XListViewFooter.STATE_LOADING);
if (mListViewListener != null) {
mListViewListener.onLoadMore();
}
} public void setXListViewListener(IXListViewListener l) {
mListViewListener = l;
} public interface IXListViewListener { public void onRefresh(); public void onLoadMore();
}
}

在三个构造函数中,都调用initView进行了header和footer的初始化,并且定义了一个Scroller,并传入了一个减速的插值器,为了模仿回弹效果。在initView方法里面,因为header可能还没初始化完毕,所以通过GlobalLayoutlistener来获取了header的高度,然后addHeaderView添加到了listview上面。

通过重写setAdapter方法,保证Footer最后天假,并且只添加一次。

最重要的,要属onTouchEvent了。在方法开始之前,通过getAdapter().getCount()获取到了item的总数,便于计算位置。这个操作在源代码中是通过scrollerListener完成的,因为ScrollerListener在这里没大有用,所以我直接去掉了,然后把位置改到了这里。如果在setAdapter里面获取的话,只能获取到没有header和footer的item数量。

在ACTION_DOWN里面,进行了lastY的初始化,lastY是为了判断移动方向的,因为在ACTION_MOVE里面,通过ev.getRawY()-lastY可以计算出手指的移动趋势,如果>0,那么就是向下滑动,反之向上。getRowY()是获取元Y坐标,意思就是和Window和View坐标没有关系的坐标,代表在屏幕上的绝对位置。然后在下面的代码里面,如果第一项可见并且header的可见高度>0或者是向下滑动,就说明用户在向下拉动或者是向上拉动header,也就是指示箭头显示的时候的状态,这时候调用了updateHeaderHeight,来更新header的高度,实现header可以跟随手指动作上下移动。这里有个OFFSET_RADIO,这个值是一个移动比例,就是说,你手指在Y方向上移动400px,如果比例是2,那么屏幕上的控件移动就是400px/2=200px,可以通过这个值来控制用户的滑动体验。下面的关于footer的判断与此类似,不再赘述。

当用户移开手指之后,ACTION_UP方法就会被调用。在这里面,只对可见位置是0和item总数-1的位置进行了处理,其实正好对应header和footer。如果位置是0,并且可以刷新,然后当前的header可见高度>原始高度的话,就说明用户确实是要进行刷新操作,所以通过setState改变header的状态,如果有监听器的话,就调用onRefresh方法,然后调用resetHeaderHeight初始化header的状态,因为footer的操作如出一辙,所以不再赘述。但是在footer中有一个PULL_LOAD_MORE_DELTA,这个值是加载更多触发条件的临界值,只有footer的间隔超过这个值之后,才能够触发加载更多的功能,因此我们可以修改这个值来改变用户体验。

说到现在,大家应该明白基本的原理了,其实XListView就是通过对用户手势的方向和距离的判断,来动态的改变Header和Footer实现的功能,所以如果我们也有类似的需求,就可以参照这种思路进行自定义。

下面再说几个比较重要的方法。

前面我们说道,在ACTION_MOVE里面,会不断的调用下面的updateXXXX方法,来动态的改变header和fooer的状态,

private void updateHeaderHeight(float delta) {
headerView.setVisiableHeight((int) delta
+ headerView.getVisiableHeight());
// 未处于刷新状态,更新箭头
if (enableRefresh && !isRefreashing) {
if (headerView.getVisiableHeight() > headerHeight) {
headerView.setState(XListViewHeader.STATE_READY);
} else {
headerView.setState(XListViewHeader.STATE_NORMAL);
}
} } private void updateFooterHeight(float delta) {
int height = footerView.getBottomMargin() + (int) delta;
if (enableLoadMore && !isLoadingMore) {
if (height > PULL_LOAD_MORE_DELTA) {
footerView.setState(XListViewFooter.STATE_READY);
} else {
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
footerView.setBottomMargin(height); }

在移开手指之后,会调用下面的resetXXX来初始化header和footer的状态

private void resetHeaderHeight() {
// 当前的可见高度
int height = headerView.getVisiableHeight();
// 如果正在刷新并且高度没有完全展示
if ((isRefreashing && height <= headerHeight) || (height == )) {
return;
}
// 默认会回滚到header的位置
int finalHeight = ;
// 如果是正在刷新状态,则回滚到header的高度
if (isRefreashing && height > headerHeight) {
finalHeight = headerHeight;
}
mScrollBack = SCROLLBACK_HEADER;
// 回滚到指定位置
scroller.startScroll(, height, , finalHeight - height,
SCROLL_DURATION);
// 触发computeScroll
invalidate();
} private void resetFooterHeight() {
int bottomMargin = footerView.getBottomMargin();
if (bottomMargin > ) {
mScrollBack = SCROLLBACK_FOOTER;
scroller.startScroll(, bottomMargin, , -bottomMargin,
SCROLL_DURATION);
invalidate();
}
}

我们可以看到,滚动操作不是通过直接的设置高度来实现的,而是通过Scroller.startScroll()来实现的,通过调用此方法,computeScroll()就会被调用,然后在这个里面,根据mScrollBack区分是哪一个滚动,然后再通过设置高度和间隔,就可以完成收缩的效果了。
    至此,整个XListView的实现原理就完全的搞明白了,以后如果做滚动类的自定义控件,应该也有思路了。

  1. private void resetHeaderHeight() {
  2. // 当前的可见高度
  3. int height = headerView.getVisiableHeight();
  4. // 如果正在刷新并且高度没有完全展示
  5. if ((isRefreashing && height <= headerHeight) || (height == 0)) {
  6. return;
  7. }
  8. // 默认会回滚到header的位置
  9. int finalHeight = 0;
  10. // 如果是正在刷新状态,则回滚到header的高度
  11. if (isRefreashing && height > headerHeight) {
  12. finalHeight = headerHeight;
  13. }
  14. mScrollBack = SCROLLBACK_HEADER;
  15. // 回滚到指定位置
  16. scroller.startScroll(0, height, 0, finalHeight - height,
  17. SCROLL_DURATION);
  18. // 触发computeScroll
  19. invalidate();
  20. }
  21. private void resetFooterHeight() {
  22. int bottomMargin = footerView.getBottomMargin();
  23. if (bottomMargin > 0) {
  24. mScrollBack = SCROLLBACK_FOOTER;
  25. scroller.startScroll(0, bottomMargin, 0, -bottomMargin,
  26. SCROLL_DURATION);
  27. invalidate();
  28. }
  29. }

Android XListView实现原理讲解及分析的更多相关文章

  1. Android Touch事件原理加实例分析

    Android中有各种各样的事件,以响应用户的操作.这些事件可以分为按键事件和触屏事件.而Touch事件是触屏事件的基础事件,在进行Android开发时经常会用到,所以非常有必要深入理解它的原理机制. ...

  2. Android AsyncTask运作原理和源码分析

    自10年大量看源码后,很少看了,抽时间把最新的源码看看! public abstract class AsyncTask<Params, Progress, Result> {     p ...

  3. Android ANR从原理到日志分析,记下来就够了

    站在巨人的肩膀上可以看的更远 做一个优秀的搬运工 Android 彻底理解安卓应用无响应机制 Android ANR日志分析全面解析 优秀的文章不可独享,要扩散,要做好笔记,哈 <沁园春长沙&g ...

  4. Android Xlistview的源码浅度分析 监听ListView上下滑动 以及是否到顶和底部

    如转载 请注明出处 http://blog.csdn.net/sk719887916 比如我们很多项目中会用到listview 并且要对listview滑动方向进行判断 也有需要的到listview是 ...

  5. 转——Android应用开发性能优化完全分析

    [工匠若水 http://blog.csdn.net/yanbober 转载请注明出处.] 1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉 ...

  6. Android 应用开发性能优化完全分析

    1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结.我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只 ...

  7. Android 多渠道打包原理和使用

    每次中午吃饭总会和技术同学聊天.当做 iOS 开发的做安卓开发的人员在一起的时候,他们中间又多了一个话题:iOS 开发难还是安卓开发难. 这个时候做安卓开发的同学最激动说安卓开发要自己画界面.机型复杂 ...

  8. 【转】Android应用开发性能优化完全分析

    http://blog.csdn.net/yanbober/article/details/48394201 1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关 ...

  9. Android应用开发性能优化完全分析

    1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结.我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只 ...

随机推荐

  1. ie兼容---haslayout

    要想更好的理解 css, 尤其是 IE 下对 css 的渲染,haslayout 是一个非常有必要彻底弄清除的概念.大多IE下的显示错误,就是源于 haslayout. 什么是 haslayout ? ...

  2. wifi 攻破

    链接1 wifi 加密方式 1,wep加密 2.WPA/WPA2-PSK加密 WPA2 的破解方式: 1 爆力破解 2,pin 破解 1) 先破解 pin 码 2)再用 minidwep-gtk 破解

  3. 将warning设为错误

    在程序编写过程中,我们有时会因为现实情况将现在无法实现的部分功能设为warning #prama warning ------------------ 为了方便查找warning,或查看某部分的警告, ...

  4. WPF中PasswordBox控件无法绑定Password属性解决办法

    在WPF中,默认的Password控件的Password属性是不允许为之绑定的,下面是一个解决绑定Password的方法的代码: 1.前台代码 <Window x:Class="Pas ...

  5. ajax跨域请求的方案

    $.get("@Hosts.Default.Www/api/XXXXX/Getxxx/"+@Model.UserId, function(data) { $("#tota ...

  6. openStack CI(Continuous interaction)/CD(Continuous delivery) Gerrit/Jenkins安装及集成,插件配置

    preFace: CI/CD practice part contains the following action items and fields of expertise: Gerrit ins ...

  7. 【LeetCode】4Sum 解题报告

    [题目] Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d  ...

  8. Android App开之标注切图

    身为一个android开发狗,真是艰辛啊,适配不好做,Rom特性不好搞,连切图有时候都得自己上啊,设计师MM都不敢去惹呢,新技能Get开始. 其实,都是小case了,我有度娘和谷哥! 因为,有了psd ...

  9. Entity Framework Batch Update

      NuGet Package PM> Install-Package EntityFramework.Extended NuGet: http://nuget.org/List/Package ...

  10. Linux下Oracle常见安装错误[Z]

    #./runInstaller之后出现如下的错误信息: RedHat AS5 x86上安装Oracle1020 Exception in thread "main" java.la ...