Advertisement
Guest User

sticky

a guest
Oct 22nd, 2014
138
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.01 KB | None | 0 0
  1. package com.emilsjolander.components.stickylistheaders;
  2.  
  3. import java.lang.reflect.Field;
  4. import java.util.ArrayList;
  5.  
  6. import android.annotation.SuppressLint;
  7. import android.content.Context;
  8. import android.database.DataSetObserver;
  9. import android.graphics.Canvas;
  10. import android.graphics.Rect;
  11. import android.graphics.drawable.Drawable;
  12. import android.os.Build;
  13. import android.util.AttributeSet;
  14. import android.view.MotionEvent;
  15. import android.view.View;
  16. import android.view.ViewConfiguration;
  17. import android.view.ViewGroup;
  18. import android.widget.AbsListView;
  19. import android.widget.ListAdapter;
  20. import android.widget.ListView;
  21. import android.widget.SectionIndexer;
  22.  
  23. /**
  24. * @author Emil Sjölander
  25. */
  26. @SuppressLint("NewApi")
  27. public class StickyListHeadersListView extends ListView {
  28.  
  29. public interface OnHeaderClickListener {
  30. public void onHeaderClick(StickyListHeadersListView l, View header,
  31. int itemPosition, long headerId, boolean currentlySticky);
  32. }
  33.  
  34. public interface OnStickyHeaderChangedListener {
  35. void onStickyHeaderChanged(StickyListHeadersListView l, View header,
  36. int itemPosition, long headerId);
  37.  
  38. }
  39.  
  40. private OnScrollListener mOnScrollListenerDelegate;
  41. private boolean mAreHeadersSticky = true;
  42. private int mHeaderBottomPosition;
  43. private View mHeader;
  44. private int mDividerHeight;
  45. private Drawable mDivider;
  46. private Boolean mClippingToPadding;
  47. private final Rect mClippingRect = new Rect();
  48. private Long mCurrentHeaderId = null;
  49. private AdapterWrapper mAdapter;
  50. private float mHeaderDownY = -1;
  51. private boolean mHeaderBeingPressed = false;
  52. private OnHeaderClickListener mOnHeaderClickListener;
  53. private Integer mHeaderPosition;
  54. private ViewConfiguration mViewConfig;
  55. private ArrayList<View> mFooterViews;
  56. private boolean mDrawingListUnderStickyHeader = false;
  57. private Rect mSelectorRect = new Rect();// for if reflection fails
  58. private Field mSelectorPositionField;
  59. private OnStickyHeaderChangedListener mOnStickyHeaderChangedListener;
  60.  
  61. private AdapterWrapper.OnHeaderClickListener mAdapterHeaderClickListener = new AdapterWrapper.OnHeaderClickListener() {
  62.  
  63. @Override
  64. public void onHeaderClick(View header, int itemPosition, long headerId) {
  65. if (mOnHeaderClickListener != null) {
  66. mOnHeaderClickListener.onHeaderClick(
  67. StickyListHeadersListView.this, header, itemPosition,
  68. headerId, false);
  69. }
  70. }
  71. };
  72.  
  73. private DataSetObserver mDataSetChangedObserver = new DataSetObserver() {
  74. @Override
  75. public void onChanged() {
  76. reset();
  77. }
  78.  
  79. @Override
  80. public void onInvalidated() {
  81. reset();
  82. }
  83. };
  84.  
  85. private OnScrollListener mOnScrollListener = new OnScrollListener() {
  86.  
  87. @Override
  88. public void onScrollStateChanged(AbsListView view, int scrollState) {
  89. if (mOnScrollListenerDelegate != null) {
  90. mOnScrollListenerDelegate.onScrollStateChanged(view,
  91. scrollState);
  92. }
  93. }
  94.  
  95. @Override
  96. public void onScroll(AbsListView view, int firstVisibleItem,
  97. int visibleItemCount, int totalItemCount) {
  98. if (mOnScrollListenerDelegate != null) {
  99. mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
  100. visibleItemCount, totalItemCount);
  101. }
  102. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
  103. scrollChanged(firstVisibleItem);
  104. }
  105. }
  106. };
  107.  
  108. public StickyListHeadersListView(Context context) {
  109. this(context, null);
  110. }
  111.  
  112. public StickyListHeadersListView(Context context, AttributeSet attrs) {
  113. this(context, attrs, android.R.attr.listViewStyle);
  114. }
  115.  
  116. public StickyListHeadersListView(Context context, AttributeSet attrs,
  117. int defStyle) {
  118. super(context, attrs, defStyle);
  119.  
  120. super.setOnScrollListener(mOnScrollListener);
  121. // null out divider, dividers are handled by adapter so they look good
  122. // with headers
  123. super.setDivider(null);
  124. super.setDividerHeight(0);
  125. mViewConfig = ViewConfiguration.get(context);
  126. if (mClippingToPadding == null) {
  127. mClippingToPadding = true;
  128. }
  129.  
  130. try {
  131. Field selectorRectField = AbsListView.class
  132. .getDeclaredField("mSelectorRect");
  133. selectorRectField.setAccessible(true);
  134. mSelectorRect = (Rect) selectorRectField.get(this);
  135.  
  136. mSelectorPositionField = AbsListView.class
  137. .getDeclaredField("mSelectorPosition");
  138. mSelectorPositionField.setAccessible(true);
  139. } catch (NoSuchFieldException e) {
  140. e.printStackTrace();
  141. } catch (IllegalArgumentException e) {
  142. e.printStackTrace();
  143. } catch (IllegalAccessException e) {
  144. e.printStackTrace();
  145. }
  146. }
  147.  
  148. @Override
  149. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  150. super.onLayout(changed, l, t, r, b);
  151. if (changed) {
  152. reset();
  153. scrollChanged(getFirstVisiblePosition());
  154. }
  155. }
  156.  
  157. private void reset() {
  158. mHeader = null;
  159. mCurrentHeaderId = null;
  160. mHeaderPosition = null;
  161. mHeaderBottomPosition = -1;
  162. }
  163.  
  164. @Override
  165. public boolean performItemClick(View view, int position, long id) {
  166. if (view instanceof WrapperView) {
  167. view = ((WrapperView) view).mItem;
  168. }
  169. return super.performItemClick(view, position, id);
  170. }
  171.  
  172. @Override
  173. public void setSelectionFromTop(int position, int y) {
  174. if (hasStickyHeaderAtPosition(position)) {
  175. y += getHeaderHeight();
  176. }
  177. super.setSelectionFromTop(position, y);
  178. }
  179.  
  180. @SuppressLint("NewApi")
  181. @Override
  182. public void smoothScrollToPositionFromTop(int position, int offset) {
  183. if (hasStickyHeaderAtPosition(position)) {
  184. offset += getHeaderHeight();
  185. }
  186. super.smoothScrollToPositionFromTop(position, offset);
  187. }
  188.  
  189. @SuppressLint("NewApi")
  190. @Override
  191. public void smoothScrollToPositionFromTop(int position, int offset,
  192. int duration) {
  193. if (hasStickyHeaderAtPosition(position)) {
  194. offset += getHeaderHeight();
  195. }
  196. super.smoothScrollToPositionFromTop(position, offset, duration);
  197. }
  198.  
  199. private boolean hasStickyHeaderAtPosition(int position) {
  200. position -= getHeaderViewsCount();
  201. return mAreHeadersSticky
  202. && position > 0
  203. && position < mAdapter.getCount()
  204. && mAdapter.getHeaderId(position) == mAdapter
  205. .getHeaderId(position - 1);
  206. }
  207.  
  208. @Override
  209. public void setDivider(Drawable divider) {
  210. this.mDivider = divider;
  211. if (divider != null) {
  212. int dividerDrawableHeight = divider.getIntrinsicHeight();
  213. if (dividerDrawableHeight >= 0) {
  214. setDividerHeight(dividerDrawableHeight);
  215. }
  216. }
  217. if (mAdapter != null) {
  218. mAdapter.setDivider(divider);
  219. requestLayout();
  220. invalidate();
  221. }
  222. }
  223.  
  224. @Override
  225. public void setDividerHeight(int height) {
  226. mDividerHeight = height;
  227. if (mAdapter != null) {
  228. mAdapter.setDividerHeight(height);
  229. requestLayout();
  230. invalidate();
  231. }
  232. }
  233.  
  234. @Override
  235. public void setOnScrollListener(OnScrollListener l) {
  236. mOnScrollListenerDelegate = l;
  237. }
  238.  
  239. public void setAreHeadersSticky(boolean areHeadersSticky) {
  240. if (this.mAreHeadersSticky != areHeadersSticky) {
  241. this.mAreHeadersSticky = areHeadersSticky;
  242. requestLayout();
  243. }
  244. }
  245.  
  246. public boolean getAreHeadersSticky() {
  247. return mAreHeadersSticky;
  248. }
  249.  
  250. @Override
  251. public void setAdapter(ListAdapter adapter) {
  252. if (this.isInEditMode()) {
  253. super.setAdapter(adapter);
  254. return;
  255. }
  256. if (adapter == null) {
  257. mAdapter = null;
  258. reset();
  259. super.setAdapter(null);
  260. return;
  261. }
  262. if (!(adapter instanceof StickyListHeadersAdapter)) {
  263. throw new IllegalArgumentException(
  264. "Adapter must implement StickyListHeadersAdapter");
  265. }
  266. mAdapter = wrapAdapter(adapter);
  267. reset();
  268. super.setAdapter(this.mAdapter);
  269. }
  270.  
  271. private AdapterWrapper wrapAdapter(ListAdapter adapter) {
  272. AdapterWrapper wrapper;
  273. if (adapter instanceof SectionIndexer) {
  274. wrapper = new SectionIndexerAdapterWrapper(getContext(),
  275. (StickyListHeadersAdapter) adapter);
  276. } else {
  277. wrapper = new AdapterWrapper(getContext(),
  278. (StickyListHeadersAdapter) adapter);
  279. }
  280. wrapper.setDivider(mDivider);
  281. wrapper.setDividerHeight(mDividerHeight);
  282. wrapper.registerDataSetObserver(mDataSetChangedObserver);
  283. wrapper.setOnHeaderClickListener(mAdapterHeaderClickListener);
  284. return wrapper;
  285. }
  286.  
  287. public StickyListHeadersAdapter getWrappedAdapter() {
  288. return mAdapter == null ? null : mAdapter.mDelegate;
  289. }
  290.  
  291. public View getWrappedView(int position) {
  292. View view = getChildAt(position);
  293. if ((view instanceof WrapperView))
  294. return ((WrapperView) view).mItem;
  295. return view;
  296. }
  297.  
  298. @Override
  299. protected void dispatchDraw(Canvas canvas) {
  300. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
  301. scrollChanged(getFirstVisiblePosition());
  302. }
  303. positionSelectorRect();
  304. if (!mAreHeadersSticky || mHeader == null) {
  305. super.dispatchDraw(canvas);
  306. return;
  307. }
  308.  
  309. if (!mDrawingListUnderStickyHeader) {
  310. mClippingRect
  311. .set(0, mHeaderBottomPosition, getWidth(), getHeight());
  312. canvas.save();
  313. canvas.clipRect(mClippingRect);
  314. }
  315.  
  316. super.dispatchDraw(canvas);
  317.  
  318. if (!mDrawingListUnderStickyHeader) {
  319. canvas.restore();
  320. }
  321.  
  322. drawStickyHeader(canvas);
  323. }
  324.  
  325. private void positionSelectorRect() {
  326. if (!mSelectorRect.isEmpty()) {
  327. int selectorPosition = getSelectorPosition();
  328. if (selectorPosition >= 0) {
  329. int firstVisibleItem = fixedFirstVisibleItem(getFirstVisiblePosition());
  330. View v = getChildAt(selectorPosition - firstVisibleItem);
  331. if (v instanceof WrapperView) {
  332. WrapperView wrapper = ((WrapperView) v);
  333. mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop;
  334. }
  335. }
  336. }
  337. }
  338.  
  339. private int getSelectorPosition() {
  340. if (mSelectorPositionField == null) { // not all supported andorid
  341. // version have this variable
  342. for (int i = 0; i < getChildCount(); i++) {
  343. if (getChildAt(i).getBottom() == mSelectorRect.bottom) {
  344. return i + fixedFirstVisibleItem(getFirstVisiblePosition());
  345. }
  346. }
  347. } else {
  348. try {
  349. return mSelectorPositionField.getInt(this);
  350. } catch (IllegalArgumentException e) {
  351. e.printStackTrace();
  352. } catch (IllegalAccessException e) {
  353. e.printStackTrace();
  354. }
  355. }
  356. return -1;
  357. }
  358.  
  359. private void drawStickyHeader(Canvas canvas) {
  360. int headerHeight = getHeaderHeight();
  361. int top = mHeaderBottomPosition - headerHeight;
  362. // clip the headers drawing region
  363. mClippingRect.left = getPaddingLeft();
  364. mClippingRect.right = getWidth() - getPaddingRight();
  365. mClippingRect.bottom = top + headerHeight;
  366. mClippingRect.top = mClippingToPadding ? getPaddingTop() : 0;
  367.  
  368. canvas.save();
  369. canvas.clipRect(mClippingRect);
  370. canvas.translate(getPaddingLeft(), top);
  371. mHeader.draw(canvas);
  372. canvas.restore();
  373. }
  374.  
  375. private void measureHeader() {
  376.  
  377. int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth()
  378. - getPaddingLeft() - getPaddingRight()
  379. - (isScrollBarOverlay() ? 0 : getVerticalScrollbarWidth()),
  380. MeasureSpec.EXACTLY);
  381. int heightMeasureSpec = 0;
  382.  
  383. ViewGroup.LayoutParams params = mHeader.getLayoutParams();
  384. if (params == null) {
  385. mHeader.setLayoutParams(new ViewGroup.MarginLayoutParams(
  386. ViewGroup.LayoutParams.MATCH_PARENT,
  387. ViewGroup.LayoutParams.WRAP_CONTENT));
  388.  
  389. }
  390. if (params != null && params.height > 0) {
  391. heightMeasureSpec = MeasureSpec.makeMeasureSpec(params.height,
  392. MeasureSpec.EXACTLY);
  393. } else {
  394. heightMeasureSpec = MeasureSpec.makeMeasureSpec(0,
  395. MeasureSpec.UNSPECIFIED);
  396. }
  397. mHeader.measure(widthMeasureSpec, heightMeasureSpec);
  398. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
  399. mHeader.setLayoutDirection(this.getLayoutDirection());
  400. }
  401.  
  402. mHeader.layout(getPaddingLeft(), 0, getWidth() - getPaddingRight(),
  403. mHeader.getMeasuredHeight());
  404. }
  405.  
  406. @SuppressLint("NewApi")
  407. private boolean isScrollBarOverlay() {
  408. int scrollBarStyle = getScrollBarStyle();
  409. return scrollBarStyle == SCROLLBARS_INSIDE_OVERLAY
  410. || scrollBarStyle == SCROLLBARS_OUTSIDE_OVERLAY;
  411. }
  412.  
  413. private int getHeaderHeight() {
  414. return mHeader == null ? 0 : mHeader.getMeasuredHeight();
  415. }
  416.  
  417. @Override
  418. public void setClipToPadding(boolean clipToPadding) {
  419. super.setClipToPadding(clipToPadding);
  420. mClippingToPadding = clipToPadding;
  421. }
  422.  
  423. private void scrollChanged(int reportedFirstVisibleItem) {
  424.  
  425. int adapterCount = mAdapter == null ? 0 : mAdapter.getCount();
  426. if (adapterCount == 0 || !mAreHeadersSticky) {
  427. return;
  428. }
  429.  
  430. final int listViewHeaderCount = getHeaderViewsCount();
  431. final int firstVisibleItem = fixedFirstVisibleItem(reportedFirstVisibleItem)
  432. - listViewHeaderCount;
  433.  
  434. if (firstVisibleItem < 0 || firstVisibleItem > adapterCount - 1) {
  435. reset();
  436. updateHeaderVisibilities();
  437. invalidate();
  438. return;
  439. }
  440.  
  441. if (mHeaderPosition == null || mHeaderPosition != firstVisibleItem) {
  442. mHeaderPosition = firstVisibleItem;
  443. mCurrentHeaderId = mAdapter.getHeaderId(firstVisibleItem);
  444. mHeader = mAdapter.getHeaderView(mHeaderPosition, mHeader, this);
  445. measureHeader();
  446.  
  447. if(mOnStickyHeaderChangedListener != null) {
  448. mOnStickyHeaderChangedListener.onStickyHeaderChanged(this,mHeader, mHeaderPosition, mCurrentHeaderId);
  449. }
  450. }
  451.  
  452. int childCount = getChildCount();
  453. if (childCount != 0) {
  454. View viewToWatch = null;
  455. int watchingChildDistance = Integer.MAX_VALUE;
  456. boolean viewToWatchIsFooter = false;
  457.  
  458. for (int i = 0; i < childCount; i++) {
  459. final View child = super.getChildAt(i);
  460. final boolean childIsFooter = mFooterViews != null
  461. && mFooterViews.contains(child);
  462.  
  463. final int childDistance = child.getTop()
  464. - (mClippingToPadding ? getPaddingTop() : 0);
  465. if (childDistance < 0) {
  466. continue;
  467. }
  468.  
  469. if (viewToWatch == null
  470. || (!viewToWatchIsFooter && !((WrapperView) viewToWatch)
  471. .hasHeader())
  472. || ((childIsFooter || ((WrapperView) child).hasHeader()) && childDistance < watchingChildDistance)) {
  473. viewToWatch = child;
  474. viewToWatchIsFooter = childIsFooter;
  475. watchingChildDistance = childDistance;
  476. }
  477. }
  478.  
  479. final int headerHeight = getHeaderHeight();
  480. if (viewToWatch != null
  481. && (viewToWatchIsFooter || ((WrapperView) viewToWatch)
  482. .hasHeader())) {
  483. if (firstVisibleItem == listViewHeaderCount
  484. && super.getChildAt(0).getTop() > 0
  485. && !mClippingToPadding) {
  486. mHeaderBottomPosition = 0;
  487. } else {
  488. final int paddingTop = mClippingToPadding ? getPaddingTop()
  489. : 0;
  490. mHeaderBottomPosition = Math.min(viewToWatch.getTop(),
  491. headerHeight + paddingTop);
  492. mHeaderBottomPosition = mHeaderBottomPosition < paddingTop ? headerHeight
  493. + paddingTop
  494. : mHeaderBottomPosition;
  495. }
  496. } else {
  497. mHeaderBottomPosition = headerHeight
  498. + (mClippingToPadding ? getPaddingTop() : 0);
  499. }
  500. }
  501. updateHeaderVisibilities();
  502. invalidate();
  503. }
  504.  
  505. @Override
  506. public void addFooterView(View v) {
  507. super.addFooterView(v);
  508. if (mFooterViews == null) {
  509. mFooterViews = new ArrayList<View>();
  510. }
  511. mFooterViews.add(v);
  512. }
  513.  
  514. @Override
  515. public boolean removeFooterView(View v) {
  516. if (super.removeFooterView(v)) {
  517. mFooterViews.remove(v);
  518. return true;
  519. }
  520. return false;
  521. }
  522.  
  523. private void updateHeaderVisibilities() {
  524. int top = mClippingToPadding ? getPaddingTop() : 0;
  525. int childCount = getChildCount();
  526. for (int i = 0; i < childCount; i++) {
  527. View child = super.getChildAt(i);
  528. if (child instanceof WrapperView) {
  529. WrapperView wrapperViewChild = (WrapperView) child;
  530. if (wrapperViewChild.hasHeader()) {
  531. View childHeader = wrapperViewChild.mHeader;
  532. if (wrapperViewChild.getTop() < top) {
  533. childHeader.setVisibility(View.INVISIBLE);
  534. } else {
  535. childHeader.setVisibility(View.VISIBLE);
  536. }
  537. }
  538. }
  539. }
  540. }
  541.  
  542. private int fixedFirstVisibleItem(int firstVisibleItem) {
  543. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
  544. return firstVisibleItem;
  545. }
  546.  
  547. for (int i = 0; i < getChildCount(); i++) {
  548. if (getChildAt(i).getBottom() >= 0) {
  549. firstVisibleItem += i;
  550. break;
  551. }
  552. }
  553.  
  554. // work around to fix bug with firstVisibleItem being to high because
  555. // listview does not take clipToPadding=false into account
  556. if (!mClippingToPadding && getPaddingTop() > 0) {
  557. if (super.getChildAt(0).getTop() > 0) {
  558. if (firstVisibleItem > 0) {
  559. firstVisibleItem -= 1;
  560. }
  561. }
  562. }
  563. return firstVisibleItem;
  564. }
  565.  
  566. public void setOnHeaderClickListener(
  567. OnHeaderClickListener onHeaderClickListener) {
  568. this.mOnHeaderClickListener = onHeaderClickListener;
  569. }
  570.  
  571. public void setDrawingListUnderStickyHeader(
  572. boolean drawingListUnderStickyHeader) {
  573. mDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
  574. }
  575.  
  576. public boolean isDrawingListUnderStickyHeader() {
  577. return mDrawingListUnderStickyHeader;
  578. }
  579.  
  580. // TODO handle touches better, multitouch etc.
  581. @Override
  582. public boolean onTouchEvent(MotionEvent ev) {
  583. int action = ev.getAction();
  584. if (action == MotionEvent.ACTION_DOWN
  585. && ev.getY() <= mHeaderBottomPosition) {
  586. mHeaderDownY = ev.getY();
  587. mHeaderBeingPressed = true;
  588. mHeader.setPressed(true);
  589. mHeader.invalidate();
  590. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  591. return true;
  592. }
  593. if (mHeaderBeingPressed) {
  594. if (Math.abs(ev.getY() - mHeaderDownY) < mViewConfig
  595. .getScaledTouchSlop()) {
  596. if (action == MotionEvent.ACTION_UP
  597. || action == MotionEvent.ACTION_CANCEL) {
  598. mHeaderDownY = -1;
  599. mHeaderBeingPressed = false;
  600. mHeader.setPressed(false);
  601. mHeader.invalidate();
  602. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  603. if (mOnHeaderClickListener != null) {
  604. mOnHeaderClickListener.onHeaderClick(this, mHeader,
  605. mHeaderPosition, mCurrentHeaderId, true);
  606. }
  607. }
  608. return true;
  609. } else {
  610. mHeaderDownY = -1;
  611. mHeaderBeingPressed = false;
  612. mHeader.setPressed(false);
  613. mHeader.invalidate();
  614. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  615. }
  616. }
  617. boolean result = false;
  618. try {
  619. result = super.onTouchEvent(ev);
  620. } catch (Exception e) {}
  621. return result;
  622. }
  623.  
  624. public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener) {
  625. mOnStickyHeaderChangedListener = listener;
  626. }
  627. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement