Advertisement
Guest User

sticky

a guest
Oct 23rd, 2014
154
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.99 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. if(mOnStickyHeaderChangedListener != null) {
  446. mOnStickyHeaderChangedListener.onStickyHeaderChanged(this, mHeader, mHeaderPosition, 0);
  447. }
  448. measureHeader();
  449. }
  450.  
  451. int childCount = getChildCount();
  452. if (childCount != 0) {
  453. View viewToWatch = null;
  454. int watchingChildDistance = Integer.MAX_VALUE;
  455. boolean viewToWatchIsFooter = false;
  456.  
  457. for (int i = 0; i < childCount; i++) {
  458. final View child = super.getChildAt(i);
  459. final boolean childIsFooter = mFooterViews != null
  460. && mFooterViews.contains(child);
  461.  
  462. final int childDistance = child.getTop()
  463. - (mClippingToPadding ? getPaddingTop() : 0);
  464. if (childDistance < 0) {
  465. continue;
  466. }
  467.  
  468. if (viewToWatch == null
  469. || (!viewToWatchIsFooter && !((WrapperView) viewToWatch)
  470. .hasHeader())
  471. || ((childIsFooter || ((WrapperView) child).hasHeader()) && childDistance < watchingChildDistance)) {
  472. viewToWatch = child;
  473. viewToWatchIsFooter = childIsFooter;
  474. watchingChildDistance = childDistance;
  475. }
  476. }
  477.  
  478. final int headerHeight = getHeaderHeight();
  479. if (viewToWatch != null
  480. && (viewToWatchIsFooter || ((WrapperView) viewToWatch)
  481. .hasHeader())) {
  482. if (firstVisibleItem == listViewHeaderCount
  483. && super.getChildAt(0).getTop() > 0
  484. && !mClippingToPadding) {
  485. mHeaderBottomPosition = 0;
  486. } else {
  487. final int paddingTop = mClippingToPadding ? getPaddingTop()
  488. : 0;
  489. mHeaderBottomPosition = Math.min(viewToWatch.getTop(),
  490. headerHeight + paddingTop);
  491. mHeaderBottomPosition = mHeaderBottomPosition < paddingTop ? headerHeight
  492. + paddingTop
  493. : mHeaderBottomPosition;
  494. }
  495. } else {
  496. mHeaderBottomPosition = headerHeight
  497. + (mClippingToPadding ? getPaddingTop() : 0);
  498. }
  499. }
  500. updateHeaderVisibilities();
  501. invalidate();
  502. }
  503.  
  504. @Override
  505. public void addFooterView(View v) {
  506. super.addFooterView(v);
  507. if (mFooterViews == null) {
  508. mFooterViews = new ArrayList<View>();
  509. }
  510. mFooterViews.add(v);
  511. }
  512.  
  513. @Override
  514. public boolean removeFooterView(View v) {
  515. if (super.removeFooterView(v)) {
  516. mFooterViews.remove(v);
  517. return true;
  518. }
  519. return false;
  520. }
  521.  
  522. private void updateHeaderVisibilities() {
  523. int top = mClippingToPadding ? getPaddingTop() : 0;
  524. int childCount = getChildCount();
  525. for (int i = 0; i < childCount; i++) {
  526. View child = super.getChildAt(i);
  527. if (child instanceof WrapperView) {
  528. WrapperView wrapperViewChild = (WrapperView) child;
  529. if (wrapperViewChild.hasHeader()) {
  530. View childHeader = wrapperViewChild.mHeader;
  531. if (wrapperViewChild.getTop() < top) {
  532. childHeader.setVisibility(View.INVISIBLE);
  533. } else {
  534. childHeader.setVisibility(View.VISIBLE);
  535. }
  536. }
  537. }
  538. }
  539. }
  540.  
  541. private int fixedFirstVisibleItem(int firstVisibleItem) {
  542. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
  543. return firstVisibleItem;
  544. }
  545.  
  546. for (int i = 0; i < getChildCount(); i++) {
  547. if (getChildAt(i).getBottom() >= 0) {
  548. firstVisibleItem += i;
  549. break;
  550. }
  551. }
  552.  
  553. // work around to fix bug with firstVisibleItem being to high because
  554. // listview does not take clipToPadding=false into account
  555. if (!mClippingToPadding && getPaddingTop() > 0) {
  556. if (super.getChildAt(0).getTop() > 0) {
  557. if (firstVisibleItem > 0) {
  558. firstVisibleItem -= 1;
  559. }
  560. }
  561. }
  562. return firstVisibleItem;
  563. }
  564.  
  565. public void setOnHeaderClickListener(
  566. OnHeaderClickListener onHeaderClickListener) {
  567. this.mOnHeaderClickListener = onHeaderClickListener;
  568. }
  569.  
  570. public void setDrawingListUnderStickyHeader(
  571. boolean drawingListUnderStickyHeader) {
  572. mDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
  573. }
  574.  
  575. public boolean isDrawingListUnderStickyHeader() {
  576. return mDrawingListUnderStickyHeader;
  577. }
  578.  
  579. // TODO handle touches better, multitouch etc.
  580. @Override
  581. public boolean onTouchEvent(MotionEvent ev) {
  582. int action = ev.getAction();
  583. if (action == MotionEvent.ACTION_DOWN
  584. && ev.getY() <= mHeaderBottomPosition) {
  585. mHeaderDownY = ev.getY();
  586. mHeaderBeingPressed = true;
  587. mHeader.setPressed(true);
  588. mHeader.invalidate();
  589. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  590. return true;
  591. }
  592. if (mHeaderBeingPressed) {
  593. if (Math.abs(ev.getY() - mHeaderDownY) < mViewConfig
  594. .getScaledTouchSlop()) {
  595. if (action == MotionEvent.ACTION_UP
  596. || action == MotionEvent.ACTION_CANCEL) {
  597. mHeaderDownY = -1;
  598. mHeaderBeingPressed = false;
  599. mHeader.setPressed(false);
  600. mHeader.invalidate();
  601. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  602. if (mOnHeaderClickListener != null) {
  603. mOnHeaderClickListener.onHeaderClick(this, mHeader,
  604. mHeaderPosition, mCurrentHeaderId, true);
  605. }
  606. }
  607. return true;
  608. } else {
  609. mHeaderDownY = -1;
  610. mHeaderBeingPressed = false;
  611. mHeader.setPressed(false);
  612. mHeader.invalidate();
  613. invalidate(0, 0, getWidth(), mHeaderBottomPosition);
  614. }
  615. }
  616. boolean result = false;
  617. try {
  618. result = super.onTouchEvent(ev);
  619. } catch (Exception e) {}
  620. return result;
  621. }
  622.  
  623. public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener) {
  624. mOnStickyHeaderChangedListener = listener;
  625. }
  626. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement