[Android] RecyclerView를 사용한 우아한 띄우기 타이틀 메신저

12498 단어
ContactList는 커뮤니케이션 기록을 모방하여 만든 app demo입니다.
주요 기술 포인트는 RecyclerView와 사용자 정의view로 제목 헤더, 네비게이션 사이드바를 실현했다
프로젝트 주소:https://github.com/hgDendi/ContactsList스타와 포크를 환영합니다.
인터페이스 개요:
ContactsListDemo
ContactsListDemo2
개요
contactsListStructure
그림에서는 주로 두 부분으로 간단히 나뉜다.
데이터 소스 및 인터페이스 구성 요소
데이터 원본은 주로 휴대전화의 통신록 정보에서 나온 것으로Content Resolver를 통해 얻을 수 있다.
인터페이스 구성 요소는 주로 디스플레이 목록과 사이드바가 있다.목록의 그룹 표시줄의 그리기와 현실에 중점을 두었기 때문에 ItemDecoration에 의해 실현된 것도 어려운 점이다.
재사용 방법
FloatingBarItemDecoration은 제목 표시줄을 그려야 하는position과 제목String의 맵을 전송합니다. 현재 세로, 단일 열의 목록만 지원합니다. 확장이 필요하면 이 글을 읽고 원리를 이해하면 쉽게 실현할 수 있습니다.
IndexBar에서 Label의 List로 전송되며 setListener를 통해 체크를 추가합니다.
FloatingBarItemDecoration
An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
ItemDecoration은 주로RecyclerView를 수식하는 데 사용되며adapter 데이터가 집중된 데이터 보기에 수식이나 빈자리를 증가시킨다.분할선 그리기, 효과 강조, 보이는 그룹 경계 그리기 등에 많이 쓰인다.
getItemOffset()
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = ((RecyclerView.LayoutParams))
          view.getLayoutParams()).getViewAdapterPosition();
        outRect.set(0, mList.containsKey(position) ? mTitleHeight : 0, 0, 0);
    }

그림% 1개의 캡션을 편집했습니다.주요 논리는 현재view의position을 통해 위에서 직사각형 범위를 비워야 하는지 판단하는 것입니다.
onDraw()
주로 정적 제목 표시줄 등 그리기, 즉 그룹view의 위쪽, getItem Offset () 의 영역에서 제목 표시줄을 그립니다.
@Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = 
                (RecyclerView.LayoutParams) child.getLayoutParams();
            int position = params.getViewAdapterPosition();
            if (!mList.containsKey(position)) {
                continue;
            }
            drawTitleArea(c, left, right, child, params, position);
        }
    }

onDrawOver
플로팅 그룹 난간 및 플로팅 그룹 난간 충돌 효과 그리기
전체 리스트의 드로잉 프로세스는 다음 순서를 따릅니다.
ItemDecoration#onDraw () -> ItemView 그리기 -> ItemDecoration#onDrawOver
그러므로 onDrawOver에서'부상', 즉 최상층의 효과를 만족시킬 수 있다.
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        final int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
        if (position == RecyclerView.NO_POSITION) {
            return;
        }
        View child = parent.findViewHolderForAdapterPosition(position).itemView;
        String initial = getTag(position);
        if (initial == null) {
            return;
        }

        //flag            (   gif     )
        boolean flag = false;
        if (getTag(position + 1) != null && !initial.equals(getTag(position + 1))) {
            if (child.getHeight() + child.getTop() < mTitleHeight) {
                // restore()  ,    translate                
                c.save();
                flag = true;
                //translate      ,       ,        (dy<0,      )
                c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
            }
        }

        c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
                parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mBackgroundPaint);
        c.drawText(initial, child.getPaddingLeft() + mTextStartMargin,
                parent.getPaddingTop() + mTitleHeight - (mTitleHeight - mTextHeight) / 2 - mTextBaselineOffset, mTextPaint);

        if (flag) {
            c.restore();
        }
    }

IndexBar
IndexBar는 사이드바의 구현이며 사용자 정의 View 형식입니다.
FontMatrics
그 전에 글꼴을 나타내는 행렬인 FontMatrics라는 개념을 소개했다.
BaseLine을 Text의 시작점으로 정의합니다. (영문 오선보와 비슷한 baseline)
drawText에서 전송된 세로 좌표 값도 직사각형 영역의 왼쪽 아래에 있는 세로 좌표가 아니라 BaseLine이 있는 세로 좌표입니다. (이 점은 매우 중요합니다. 그렇지 않으면 개발자 모드에서 레이아웃 경계를 열면 글꼴과 경계가 어지러워집니다.)
주로 다음과 같은 속성이 있습니다.
  • Top (<0)
  • Ascent의 가능한 최소값(절대값 최대)
  • Ascent (<0)
  • 글꼴의 가장 높은 위치BaseLine 거리
  • Descent (>0)
  • 글씨체의 최저 거리BaseLine
  • Bottom (>0)
  • Descent 가능한 최대
  • Leading
  • 간격, 여러 줄 문자 표시 거리

  • fontMatrics
    이 예에서는 View 높이를 측정하는 매개 변수로 각 text의 높이를 계산합니다.많은 경우에 leanding 값을 추가하지 않을 수 있습니다. 왜냐하면 한 줄의 여러 줄의 leading 값이 모두 0이기 때문입니다.(0이 아닌 값을 언제 얻을 수 있을지 모른다)
    Paint.FontMetrics fm = mPaint.getFontMetrics();
    float singleHeight = fm.bottom - fm.top + fm.leading;
    

    onMeasure()
    뷰의 길이와 폭을 계산합니다.
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }
    
    private int measureWidth(int widthMeasureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = getSuggestedMinWidth();
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
    
    //         ,               (           )
    private int getSuggestedMinWidth() {
        String maxLengthTag = "";
        for (String tag : mNavigators) {
            if (maxLengthTag.length() < tag.length()) {
                maxLengthTag = tag;
            }
        }
        return (int) (mPaint.measureText(maxLengthTag) + 0.5);
    }
    
    private int measureHeight(int heightMeasureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            Paint.FontMetrics fm = mPaint.getFontMetrics();
            float singleHeight = fm.bottom - fm.top + fm.leading;
            //  mLetterSpacingExtra     ,      ,  1.4
            mBaseLineHeight = fm.bottom * mLetterSpacingExtra;
            result = (int) (mNavigators.size() * singleHeight * mLetterSpacingExtra);
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
    

    onDraw()
    제작 담당
    protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int height = getHeight();
            int width = getWidth();
            //   0,           ,     
            if (height == 0) {
                return;
            }
            int singleHeight = height / mNavigators.size();
    
            //    Text
            for (int i = 0; i < mNavigators.size(); i++) {
                float xPos = width / 2 - mPaint.measureText(mNavigators.get(i)) / 2;
                float yPos = singleHeight * (i + 1);
                if (i == mFocusIndex) {
                    canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mFocusPaint);
                } else {
                    canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mPaint);
                }
            }
        }
    

    DispatchTouchEvent()
    상호작용 이벤트는 주로 UP, CANCEL, DOWN, MOVE를 감청하는데 그 중에서 DOWN을 기점으로 하고 CANCEL, UP을 종점으로 하고 나머지는 중간 상태로 한다.TAG의 초점 변경과 이벤트의 시작, 끝을 다시 그리는 촉발점으로 한다.
    @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            final float y = event.getY();
            final int formerFocusIndex = mFocusIndex;
            final OnTouchingLetterChangeListener listener = mOnTouchingLetterChangeListener;
            final int c = calculateOnClickItemNum(y);
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mFocusIndex = -1;
                    invalidate();
                    listener.onTouchingEnd(mNavigators.get(c));
                    break;
                case MotionEvent.ACTION_DOWN:
                    listener.onTouchingStart(mNavigators.get(c));
                default:
                    if (formerFocusIndex != c) {
                        if (c >= 0 && c < mNavigators.size()) {
                            listener.onTouchingLetterChanged(mNavigators.get(c));
                            mFocusIndex = c;
                            invalidate();
                        }
                    }
                    break;
            }
            return true;
        }
    
        /**
         * @param yPos
         * @return the corresponding position in list
         */
        private int calculateOnClickItemNum(float yPos) {
            int result;
            //           TAG,           (   MOVE          )
            result = (int) (yPos / getHeight() * mNavigators.size());
            if (result >= mNavigators.size()) {
                result = mNavigators.size() - 1;
            } else if (result < 0) {
                result = 0;
            }
            return result;
        }
    

    ContactsUtils
    주로 줄임말을 얻는 것을 책임지는데, 그 중에서 영문자는 바로 영문자를 얻고, 중국어 문자는 GB2312에 비해 영문 줄임말을 얻는다.
    중국어의 줄임말을 얻는 핵심 사상은 다음과 같다. GB2312가 중국어의 성모에 비해 줄임말을 얻는 경우다.
        //GB2312        ,    
        private static int BEGIN = 45217;
        private static int END = 63486;
    
        /**
         *         
         * {i、u、v}     
         */
        private static char[] chartable = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
    
        private static char[] initialtable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K','L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z'};
    
        // table         GB , initialtable  
        private static int[] table = new int[chartable.length + 1];
    
        static {
            for (int i = 0; i < chartable.length; i++) {
                table[i] = gbValue(chartable[i]);
            }
            table[chartable.length] = END;
        }
        
        //  char   gb 
        private static int gbValue(char ch) {
            String str = "" + ch;
            try {
                byte[] bytes = str.getBytes("GB2312");
                if (bytes.length < 2) {
                    return 0;
                }
                return (bytes[0] << 8 & 0xff00) + (bytes[1] & 0xff);
            } catch (Exception e) {
                return 0;
            }
        }
    

    ContactsManager
    커뮤니케이션 정보 액세스를 담당하며, 여기에는 전화 번호와 연락처 이름만 지정되며, Content Resolver를 사용하여 쿼리 수행
        @NonNull
        public static ArrayList getPhoneContacts(Context mContext) {
            ArrayList result = new ArrayList<>(0);
            ContentResolver resolver = mContext.getContentResolver();
            Cursor phoneCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, null, null, null);
            if (phoneCursor != null) {
                while (phoneCursor.moveToNext()) {
                    String phoneNumber = phoneCursor.getString(0).replace(" ", "");
                    String contactName = phoneCursor.getString(1);
                    result.add(new ShareContactsBean(contactName, phoneNumber));
                }
                phoneCursor.close();
            }
            //       ,        bean 
            Collections.sort(result, new Comparator() {
                @Override
                public int compare(ShareContactsBean l, ShareContactsBean r) {
                    return l.compareTo(r);
                }
            });
            return result;
        }
    

    좋은 웹페이지 즐겨찾기