HarmonyOS自定義控件之Material風(fēng)格的下拉刷新
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
介紹
Ohos-MaterialRefreshLayout是一個自定義Material風(fēng)格下拉刷新控件,支持設(shè)置水波紋效果,支持下拉刷新侵入式和非侵入式,初始化自動刷新及上滑加載更多,支持刷新頭部自定義圖案,上拉加載更多等。該控件一般配合ListContainer使用,因涉及事件分發(fā)操作,本庫中使用了三方控件NestedListContainer、事件分發(fā)等方便處理事件攔截分發(fā)事件。
效果圖:
自定義控件結(jié)構(gòu)
MaterialRefreshLayout控件,首先初始化設(shè)置頭部、腳部布局,在手勢下滑時顯示頭部布局,動態(tài)設(shè)置頭部高度,展示下拉刷新效果,在頁面底部向上滑動時顯示腳部布局,展示上拉加載更多效果,松手時圖形即開始旋轉(zhuǎn)動畫。
下拉圓形轉(zhuǎn)動風(fēng)格MaterialRefreshLayout
1.MaterialRefreshLayout包含自定義頭部布局MaterialHeaderView和腳部布局MaterialFooterView。
2.頭部MaterialHeaderView包含圓形轉(zhuǎn)動條CircleProgressBar和下拉波紋MaterialWaveView。
3.腳部布局MaterialFooterView同頭部結(jié)構(gòu)一致,包含圓形轉(zhuǎn)動條CircleProgressBar和下拉波紋MaterialWaveView。
4.CircleProgressBar包含有自定義圖形的MaterialProgressDrawable,設(shè)置圓形的轉(zhuǎn)動圖案。
下拉自定義笑臉風(fēng)格MaterialRefreshLayout
1.MaterialRefreshLayout包含SunLayout頭部布局和腳部布局MaterialFooterView。
2.SunLayout頭部包含滾動短線SunLineView和笑臉SunFaceView。
3.當(dāng)有手勢下滑時,自定義短線SunLineView,開始旋轉(zhuǎn)動畫,監(jiān)聽刷新動作,在onSizeChanged中動態(tài)改變圖形大小。
4.當(dāng)手勢向下滑動時,自定義笑臉圖形SunFaceView,監(jiān)聽刷新動作,在onSizeChanged中動態(tài)改變圖形大小。
代碼實(shí)現(xiàn)解讀
首先在攔截事件中根據(jù)手指的滑動距離,設(shè)置自定義頭部布局MaterialHeaderView可見,底部向上滑動時,當(dāng)滑到頁面底部,設(shè)置腳部布局MaterialFooterView可見。
事件分發(fā)onInterceptTouchEvent中設(shè)置頭、腳布局可見
在攔截事件onInterceptTouchEvent中,手指移動TouchEvent.POINT_MOVE時,根據(jù)滑動距離及是否是在頭部的滑動,設(shè)置頭部自定義headerview是否顯示,再根據(jù)向上滑動距離是否小于0及是否滑動到底部加載底部footerview。代碼如下:
- case TouchEvent.POINT_MOVE:
- float currentY = ev.getPointerPosition(0).getY();
- Float dy= new BigDecimal(currentY).subtract(new BigDecimal(mTouchY)).floatValue();
- if (dy > 0 && !canChildScrollUp()) {
- if (mMaterialHeaderView != null) {
- mMaterialHeaderView.setVisibility(Component.VISIBLE);
- mMaterialHeaderView.onBegin(this);
- } else if (mSunLayout != null) {
- mSunLayout.setVisibility(Component.VISIBLE);
- mSunLayout.onBegin(this);
- }
- return true;
- } else if (dy < 0 && !canChildScrollDown() && isLoadMore) {
- if (mMaterialFooterView != null && !isLoadMoreing) {
- soveLoadMoreLogic();
- }
- return false;
- }
- break;
上一步完成后,緊接著就是在觸摸事件中動態(tài)設(shè)置頭部布局高度,水波紋高度,滑到最大距離時,設(shè)置為控件本身高度。
事件觸摸onTouchEvent中設(shè)置高度
在觸摸事件onTouchEvent中,當(dāng)手指下滑,onTouchEvent中設(shè)置頭部自定義headerview的高度,隨著下滑距離增加,動態(tài)設(shè)置水波紋高度,當(dāng)頭部為侵入式時,設(shè)置component向下平移。代碼如下:
- case TouchEvent.POINT_MOVE:
- mCurrentY = e.getPointerPosition(0).getY();
- float dy = new BigDecimal(mCurrentY).subtract(new BigDecimal(mTouchY)).floatValue();
- dy = Math.min(mWaveHeight * 2, dy);
- dy = Math.max(0, dy);
- if (mChildView != null) {
- float offsetY = dy / 2;
- float fraction = offsetY / mHeadHeight;
- if (mMaterialHeaderView != null) {
- mMaterialHeaderView.setHeight((int) offsetY);
- mMaterialHeaderView.postLayout();
- mMaterialHeaderView.onPull(this, fraction);
- } else if (mSunLayout != null) {
- mSunLayout.setHeight((int) offsetY);
- mSunLayout.postLayout();
- mSunLayout.startSunLineAnim(this);
- mSunLayout.onPull(this, fraction);
- }
- if (!isOverlay)
- mChildView.setTranslationY(offsetY);
- }
在松手時,監(jiān)聽抬起事件TouchEvent.PRIMARY_POINT_UP,當(dāng)頭部headerview高度大于原有高度時,將頭部設(shè)置為刷新中狀態(tài),代碼如下:
- if (mMaterialHeaderView.getLayoutConfig().height > mHeadHeight) {
- updateListener();
- mMaterialHeaderView.setHeight((int) mHeadHeight);
- mMaterialHeaderView.postLayout();
- }
再接下來就是完成自定義頭部控件的布局,并在下拉接口方法中設(shè)置下拉時的縮放,透明度等狀態(tài)。
自定義頭部MaterialHeaderView
自定義MaterialHeaderView由MaterialWaveView和CircleProgressBar兩個自定義Component組合成,實(shí)現(xiàn)MaterialHeadListener接口。
onBegin方法中設(shè)置materialWaveView的起始狀態(tài),circleProgressBar縮放大小,透明度等。代碼如下:
- @Override
- public void onBegin(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onBegin(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.setScaleX(0.001f);
- circleProgressBar.setScaleY(0.001f);
- circleProgressBar.onBegin(materialRefreshLayout);
- }
- }
onPull方法中設(shè)置materialWaveView的下拉狀態(tài),circleProgressBar縮放大小,透明度等。代碼如下:
- @Override
- public void onPull(MaterialRefreshLayout materialRefreshLayout, float fraction) {
- if (materialWaveView != null) {
- materialWaveView.onPull(materialRefreshLayout, fraction);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onPull(materialRefreshLayout, fraction);
- float a = Util.limitValue(1, fraction);
- circleProgressBar.setScaleX(a);
- circleProgressBar.setScaleY(a);
- circleProgressBar.setAlpha(a);
- }
- }
設(shè)置刷新中onRefreshing狀態(tài)。代碼如下:
- @Override
- public void onRefreshing(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onRefreshing(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onRefreshing(materialRefreshLayout);
- }
- }
onComlete刷新完成后自定義Component的狀態(tài)初始化,代碼如下:
- @Override
- public void onComlete(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onComlete(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onComlete(materialRefreshLayout);
- circleProgressBar.setTranslationY(0);
- circleProgressBar.setScaleX(0);
- circleProgressBar.setScaleY(0);
- }
- }
頭部布局完成后,接下來就是實(shí)現(xiàn)自定義腳部布局實(shí)現(xiàn)。
自定義腳部MaterialFooterView
自定義MaterialFooterView由MaterialWaveView和CircleProgressBar兩個自定義Component組合成,實(shí)現(xiàn)MaterialHeadListener接口。基本同MaterialHeaderView一致,接口實(shí)現(xiàn)方法設(shè)置內(nèi)容相同。
onBegin方法中設(shè)置materialWaveView的起始狀態(tài),circleProgressBar縮放1,透明度等。代碼如下:
- @Override
- public void onBegin(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onBegin(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onBegin(materialRefreshLayout);
- circleProgressBar.setScaleX(1);
- circleProgressBar.setScaleY(1);
- }
- }
onPull方法中設(shè)置materialWaveView的下拉狀態(tài),circleProgressBar縮放1,透明度等。代碼如下:
- @Override
- public void onPull(MaterialRefreshLayout materialRefreshLayout, float fraction) {
- if (materialWaveView != null) {
- materialWaveView.onPull(materialRefreshLayout, fraction);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onPull(materialRefreshLayout, fraction);
- float a = Util.limitValue(1, fraction);
- circleProgressBar.setScaleX(1);
- circleProgressBar.setScaleY(1);
- circleProgressBar.setAlpha(a);
- }
- }
設(shè)置刷新中onRefreshing狀態(tài)。代碼如下:
- @Override
- public void onRefreshing(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onRefreshing(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onRefreshing(materialRefreshLayout);
- }
- }
onComlete刷新完成后自定義Component的狀態(tài)初始化,代碼如下:
- @Override
- public void onComlete(MaterialRefreshLayout materialRefreshLayout) {
- if (materialWaveView != null) {
- materialWaveView.onComlete(materialRefreshLayout);
- }
- if (circleProgressBar != null) {
- circleProgressBar.onComlete(materialRefreshLayout);
- circleProgressBar.setTranslationY(0);
- circleProgressBar.setScaleX(0);
- circleProgressBar.setScaleY(0);
- }
- }
頭部、腳部布局都完成后,就開始要完成頭部和腳部布局里面的自定義組件,首先從頭部布局中的自定義組件開始,前面講到頭部由圓形轉(zhuǎn)動條CircleProgressBar和下拉波紋MaterialWaveView組成,先開始繪制波浪紋MaterialWaveView,實(shí)現(xiàn)MaterialHeadListener接口,接口回調(diào)中設(shè)置組件的狀態(tài)。
自定義MaterialWaveView
初始化畫筆設(shè)置,添加addDrawTask任務(wù),onDraw方法中繪制下拉區(qū)域圖形,并填充顏色,代碼如下:
- @Override
- public void onDraw(Component component, Canvas canvas) {
- path.reset();
- paint.setColor(new Color(color));
- path.lineTo(0, headHeight);
- path.quadTo(getEstimatedWidth() / (float) 2, headHeight + waveHeight, getEstimatedWidth(), headHeight);
- path.lineTo(getEstimatedWidth(), 0);
- canvas.drawPath(path, paint);
- }
實(shí)現(xiàn)MaterialHeadListener接口,監(jiān)聽各下拉方法的回調(diào),當(dāng)有下拉的情形時,改變下拉區(qū)域狀態(tài)。下拉時在onPull中,設(shè)置下拉區(qū)域header高度及wave高度。刷新中onRefreshing,加載數(shù)值動畫并動態(tài)改變wave高度。結(jié)束onComlete中,加載數(shù)值動畫動態(tài)改變head的高度。代碼如下:
下拉時:
- @Override
- public void onPull(MaterialRefreshLayout br, float fraction) {
- setHeadHeight((int) (Util.dip2px(getContext(), DefaulHeadHeight) * Util.limitValue(1, fraction)));
- setWaveHeight((int) (Util.dip2px(getContext(), DefaulWaveHeight) * Math.max(0, new BigDecimal(fraction).subtract(new BigDecimal(1)).floatValue())));
- invalidate();
- }
刷新時:
- @Override
- public void onRefreshing(MaterialRefreshLayout br) {
- setHeadHeight((int) (Util.dip2px(getContext(), DefaulHeadHeight)));
- int waveHeight = getWaveHeight();
- AnimatorValue animator = new AnimatorValue();
- animator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
- @Override
- public void onUpdate(AnimatorValue animatorValue, float value) {
- setWaveHeight(getIntValue((1 - (double) value) * waveHeight));
- invalidate();
- }
- });
- animator.setCurveType(Animator.CurveType.BOUNCE);
- animator.setDuration(200);
- animator.start();
- }
結(jié)束時:
- @Override
- public void onComlete(MaterialRefreshLayout br) {
- waveHeight = 0;
- AnimatorValue animator = new AnimatorValue();
- animator.setDuration(200);
- animator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
- @Override
- public void onUpdate(AnimatorValue animatorValue, float value) {
- headHeight = getIntValue((1 - (double) value) * headHeight);
- invalidate();
- }
- });
- animator.start();
- }
上一步完成后接下來開始實(shí)現(xiàn)頭部圓形轉(zhuǎn)動的CircleProgressBar,并設(shè)置圖案的自定義ShapeElement圖形,配合手勢操作,下拉時設(shè)置圖形動態(tài)大小,松手時旋轉(zhuǎn)刷新。
自定義CircleProgressBar
自定義圓形轉(zhuǎn)動CircleProgressBar,設(shè)置自定義背景MaterialProgressDrawable,實(shí)現(xiàn)MaterialHeadListener接口,根據(jù)下拉狀態(tài)設(shè)置圓形MaterialProgressDrawable旋轉(zhuǎn)角度,釋放手勢時開始動畫,結(jié)束后停止旋轉(zhuǎn)并初始化狀態(tài)等。代碼如下:
- @Override
- public void onPull(MaterialRefreshLayout materialRefreshLayout, float fraction) {
- if (mProgressDrawable != null)
- mProgressDrawable.setProgressRotation(fraction);
- invalidate();
- }
- @Override
- Public void onRefreshing(MaterialRefreshLayout materialRefreshLayout) {
- if (mProgressDrawable != null) {
- mProgressDrawable.onStart();
- }
- }
- @Override
- public void onComlete(MaterialRefreshLayout materialRefreshLayout) {
- if (mProgressDrawable != null) {
- mProgressDrawable.onStop();
- }
- setVisibility(Component.INVISIBLE);
- }
自定義MaterialProgressDrawable設(shè)置CircleProgressBar的背景
首先構(gòu)造方法中初始化圓形Ring和旋轉(zhuǎn)動畫,設(shè)置畫筆顏色,寬度,大小,在drawToCanvas中繪制圓形Ring, 當(dāng)有手勢操作時調(diào)用onStart方法中的旋轉(zhuǎn)動畫,開始旋轉(zhuǎn)。在Ring類draw方法中,根據(jù)起始旋轉(zhuǎn)角度繪制圓形圈圈及三角箭頭,代碼如下:
- public void draw(Canvas c, Rect bounds) {
- final RectFloat arcBounds = mTempBounds;
- arcBounds.modify(bounds);
- arcBounds.left = new BigDecimal(arcBounds.left).add(new BigDecimal(mStrokeInset)).floatValue();
- arcBounds.top = new BigDecimal(arcBounds.top).add(new BigDecimal(mStrokeInset)).floatValue();
- arcBounds.right = new BigDecimal(arcBounds.right).subtract(new BigDecimal(mStrokeInset)).floatValue();
- arcBounds.bottom = new BigDecimal(arcBounds.bottom).subtract(new BigDecimal(mStrokeInset)).floatValue();
- final float startAngle = new BigDecimal(mStartTrim).add(new BigDecimal(mRotation)).floatValue() * 360;
- final float endAngle = new BigDecimal(mEndTrim).add(new BigDecimal(mRotation)).floatValue() * 360;
- float sweepAngle = new BigDecimal(endAngle).subtract(new BigDecimal(startAngle)).floatValue();
- mPaint.setColor(Color.RED);
- c.drawArc(arcBounds, new Arc(startAngle, sweepAngle, false), mPaint);
- drawTriangle(c, startAngle, sweepAngle, bounds);
- if (mAlpha < 255) {
- mCirclePaint.setColor(new Color(mBackgroundColor));
- mCirclePaint.setAlpha(255 - mAlpha);
- c.drawCircle(bounds.getCenterX(), bounds.getCenterY(), bounds.getWidth() / (float) 2,
- mCirclePaint);
- }
- }
上述基本上就完成了Material風(fēng)格下拉刷新帶水波紋,帶轉(zhuǎn)動progressbar的實(shí)現(xiàn)步驟,緊接著講一講下拉自定義笑臉的另外一種刷新風(fēng)格,實(shí)際上就是重新定義了刷新頭部的圖形,在這里也可以自己嘗試替換成其它不同的圖形。
自定義頭部SunLayout布局
自定義頭部SunLayout由SunFaceView和SunLineView組成,SunFaceView為自定義笑臉,SunLineView為自定義笑臉周圍短線。SunLayout實(shí)現(xiàn)了MaterialHeadListener接口,開始狀態(tài)onBegin時縮放從零到有,下拉onPull時,設(shè)置SunView和LineView的大小,縮放等。代碼如下:
自定義頭部SunLayout由SunFaceView和SunLineView組成,SunFaceView為自定義笑臉,SunLineView為自定義笑臉周圍短線。SunLayout實(shí)現(xiàn)了MaterialHeadListener接口,開始狀態(tài)onBegin時縮放從零到有,下拉onPull時,設(shè)置SunView和LineView的大小,縮放等。代碼如下:
開始時:
- @Override
- public void onBegin(MaterialRefreshLayout materialRefreshLayout) {
- setScaleX(0.001f);
- setScaleY(0.001f);
- }
下拉時:
- @Override
- public void onPull(MaterialRefreshLayout materialRefreshLayout, float fraction) {
- float a = Util.limitValue(1, fraction);
- if (a >= 0.7) {
- mLineView.setVisibility(VISIBLE);
- } else {
- mLineView.setVisibility(HIDE);
- }
- mSunView.setPerView(mSunRadius, a);
- mLineView.setLineWidth(mLineWidth);
- setScaleX(a);
- setScaleY(a);
- setAlpha(a);
- }
- 自定義笑臉SunFaceView
- 自定義短線SunLineView
SunLineView繼承Component實(shí)現(xiàn)Component.DrawTask, Component.EstimateSizeListener接口,構(gòu)造方法中初始化Paint,onEstimateSize中測量寬高,onDraw中繪制線條,代碼如下:
測量時:
- @Override
- public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
- HiLog.info(Contants.LABEL, "onMeasure");
- int widthMode = EstimateSpec.getMode(widthMeasureSpec);
- int widthSize = EstimateSpec.getSize(widthMeasureSpec);
- int heightMode = EstimateSpec.getMode(heightMeasureSpec);
- int heightSize = EstimateSpec.getSize(heightMeasureSpec);
- int width;
- int height;
- if (widthMode == EstimateSpec.PRECISE) {
- width = widthSize;
- } else {
- width = (mSunRadius + mFixLineHeight + mLineHeight) * 2 + getPaddingRight() + getPaddingLeft();
- }
- if (heightMode == EstimateSpec.PRECISE) {
- height = heightSize;
- } else {
- height = (mSunRadius + mFixLineHeight + mLineHeight) * 2 + getPaddingTop() + getPaddingBottom();
- }
- setEstimatedSize(width, height);
- mWidth = width;
- mHeight = height;
- return false;
- }
畫線條:
- private void drawLines(Canvas canvas) {
- for (int i = 0; i <= 360; i++) {
- if (i % mLineLevel == 0) {
- mLineLeft = mWidth / 2 - mLineWidth / 2;
- mLineTop = mHeight / 2 - mSunRadius - mFixLineHeight;
- mLineBottom = mLineTop + mLineHeight;
- }
- canvas.save();
- canvas.rotate(i, mWidth / (float) 2, mHeight / (float) 2);
- canvas.drawLine(mLineLeft, mLineTop, mLineLeft, mLineBottom, mLinePaint);
- canvas.restore();
- }
- }
代碼參考
https://gitee.com/chinasoft5_ohos/Ohos-MaterialRefreshLayout
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)