Erstellen eines Parallaxen-Headers in einer RecyclerView

    Grüße!
    Mit dem Aufkommen des Materialdesigns kommen auch neue Elemente. Zum Beispiel erschien RecyclerView, das viele bereits kennen. Über ihn schrieb der Habr mehr als einmal: tyts , tuts .

    Es scheint verständlich zu sein, aber Sie wollen mehr. Normalerweise fehlt beim Wechsel zu neuen Alternativen etwas. Ich hatte also nicht genug von dem, was ist. Ich musste einen Parallaxeeffekt erzielen, wie bei Google Play auf einer bestimmten Anwendungsseite. Implementierungen für ListView und ScrollView sind verfügbar. Ich schaute in die großen und mächtigen, und alles, was ich fand, war dieses Repository . Die Lösung scheint zu funktionieren, und die Leute nutzen sie. Die Benutzerfreundlichkeit hat mir jedoch nicht gefallen. Und wie immer habe ich beschlossen, meine eigenen zu schreiben.

    Im Allgemeinen habe ich mit einem einfachen begonnen, nämlich der Tatsache, dass Sie einen Adapter erstellen müssen, der den Header unterstützt. Der Adapter sollte sich nicht von den Prinzipien eines herkömmlichen Adapters für RecyclerView unterscheiden.

    Die RecyclerView.Adapter- Klasse verfügt über eine öffentliche int getItemViewType- Methode (int position) , die für jede Position einen Typ zurückgibt und standardmäßig immer 0 zurückgibt. Dies hilft uns.

    Ich muss Sie sofort warnen, dass die resultierende Klasse abstrakt sein wird. Und einige Methoden auch.

    Wir definieren es wie folgt neu:

        public static final int TYPE_VIEW_HEADER = Integer.MAX_VALUE;
        private int sizeDiff = 1;
        @Override
        public final int getItemViewType(final int position)
        {
            if (position == 0 && enableHeader)
                return TYPE_VIEW_HEADER;
            return getMainItemType(position - sizeDiff);
        }
       protected abstract int getMainItemType(int position);
    

    Der Wert TYPE_VIEW_HEADER wurde als solcher ausgewählt, um versehentliche Treffer in getMainItemType zu vermeiden .
    Logischerweise müssen Sie die Methoden implementieren, die für die Erstellung der Ansicht und die Anzeige von Informationen über uns verantwortlich sind, sowie mehrere abstrakte Methoden.
    Versteckter Text
        @Override
        public final RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType)
        {
            if (viewType == TYPE_VIEW_HEADER)
                return onCreateHeaderViewHolder(parent);
            return onCreateMainViewHolder(parent, viewType);
        }
        protected abstract HeaderHolder onCreateHeaderViewHolder(final ViewGroup parent);
        protected abstract VH onCreateMainViewHolder(final ViewGroup parent, final int viewType);
        @Override
        public final void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position)
        {
            if (holder.getItemViewType() == TYPE_VIEW_HEADER)
            {
                onBindHeaderViewHolder((HeaderHolder) holder);
                return;
            }
            onBindMainViewHolder((VH) holder, position - sizeDiff);
        }
        protected abstract  void onBindHeaderViewHolder(final HH holder);
        protected abstract void onBindMainViewHolder(final VH holder, final int position);
        protected static class HeaderHolder extends RecyclerView.ViewHolder
        {
            public HeaderHolder(final View itemView)
            {
               super(itemView);
            }
        }
    



    Ja, Casting sieht sicherlich nicht sehr gut aus, aber ich habe keinen besseren Weg gefunden, es ohne sie gleich zu lassen.

    Kurz darüber, was der Code oben tut. Zuerst geben wir den gewünschten Typ in der Methode getItemViewType an, erstellen dann basierend darauf den gewünschten ViewHolder in onCreateViewHolder und binden die Daten in onBindViewHolder, indem wir auch viewType überprüfen.

    Was geschrieben wurde, bietet bereits Funktionen, um Überschriften auf einfache Weise häufiger zu machen, aber der Titel des Artikels versprach mehr.

    Deshalb fahren wir fort.

    Die Kopfzeile wird angezeigt. Lassen Sie uns sie jetzt verschieben. Es sollte sich jedoch in die entgegengesetzte Richtung der RecyclerView-Inhaltsbewegung bewegen.

    Dazu benötigen wir einen Hilfsbehälter, der seinen Inhalt um einen bestimmten Betrag verschieben kann. Dies wird die innere Klasse unseres Adapters sein.

    Code dieser Klasse
        private static class CustomWrapper extends FrameLayout
        {
            private int offset;
            public CustomWrapper(Context context)
            {
                super(context);
            }
            @Override
            protected void dispatchDraw(Canvas canvas)
            {
                canvas.clipRect(new Rect(getLeft(), getTop(), getRight(), getBottom() + offset));
                super.dispatchDraw(canvas);
            }
            public void setYOffset(int offset)
            {
                this.offset = offset;
                invalidate();
            }
        }
    


    Dann schreiben wir unsere HeaderHolder- Klasse so um, dass sie die übergebene Ansicht in unseren Wundercontainer legt:

    HeaderHolder aktualisiert
        protected static class HeaderHolder extends RecyclerView.ViewHolder
        {
            public HeaderHolder(final View itemView)
            {
                super(new CustomWrapper(itemView.getContext()));
                final ViewGroup parent = (ViewGroup) itemView.getParent();
                if (parent != null)
                    parent.removeView(itemView);
                ((CustomWrapper) this.itemView).addView(itemView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                this.itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            }
        }
    


    Jetzt müssen nur noch die gewünschten Werte an CustomWrapper gesendet werden, und wir werden parallaxen. Dazu müssen Sie Scroll-Ereignisse bei RecyclerView abonnieren. Ich habe die innere Klasse dafür benutzt.

    Scrollrollistener
        private class ScrollListener extends RecyclerView.OnScrollListener
        {
            @Override
            public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy)
            {
                totalScroll += dy;
                if (customWrapper != null && !headerOutOfVisibleRange())
                {
                    doParallaxWithHeader(totalScroll);
                }
                changeVisibilityHeader();
            }      
            private void changeVisibilityHeader()
            {
                if (customWrapper != null)
                {
                    customWrapper.setVisibility(headerOutOfVisibleRange() ? View.INVISIBLE : View.VISIBLE);
                }
            }
            private boolean headerOutOfVisibleRange()
            {
                return totalScroll > getHeaderHeight();
            }
        }
    


    Hier ist alles einfach. Beim Scrollen wird die onScrolled-Methode aufgerufen. Darin ändern wir die aktuelle Position der Schriftrolle und prüfen, ob wir mit der Kopfzeile etwas anfangen können. Wenn ja, dann machen Sie Parallaxe. Und wenn der Header über den Bereich des Bildschirms hinausgeht, hören wir damit auf, alle Arten von Operationen auszuführen, da dies nicht erforderlich ist.

    Und die letzte Codeeinfügung
        private void doParallaxWithHeader(float offset)
        {
            float parallaxOffset = offset * SCROLL_SPEED;
            moveHeaderToOffset(parallaxOffset);
            if (parallaxListener != null && enableHeader)
                parallaxListener.onParallaxScroll(left);
            customWrapper.setYOffset(Math.round(parallaxOffset));
            notifyHeaderChanged();
        }
        private void moveHeaderToOffset(final float parallaxOffset)
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
            {
                customWrapper.setTranslationY(parallaxOffset);
            }
            else
            {
                TranslateAnimation anim = createTranslateAnimation(parallaxOffset);
                customWrapper.startAnimation(anim);
            }
        }
        private TranslateAnimation createTranslateAnimation(final float parallaxOffset)
        {
            TranslateAnimation anim = new TranslateAnimation(0, 0, parallaxOffset, parallaxOffset);
            anim.setFillAfter(true);
            anim.setDuration(0);
            return anim;
        }
        public final void notifyHeaderChanged()
        {
            notifyItemChanged(0);
        }
        public final void notifyMainItemChanged(final int position)
        {
            notifyItemChanged(position + sizeDiff);
        }
    


    Ich denke, es ist offensichtlich, dass Sie für den Parallaxeeffekt nur die "Geschwindigkeit" der Bewegung reduzieren müssen. Hierzu wird der Koeffizient SCROLL_SPEED verwendet. Dann verschieben wir einfach den Header auf den neu empfangenen Wert - und das war's.

    Die Verwendung ist sehr einfach.

    Quellen können hier entnommen werden ; ein Beispiel dort. Dies alles wird in jCenter veröffentlicht, sodass die Verbindung in einer einzelnen Zeile in Gradle hergestellt wird.

    Der Bonus ist der HeaderGridLayoutManager , der Nachfolger des GridLayoutManager, der Funktionen mit einem Header bereitstellt, und die Parallaxe funktioniert auch dort.

    Es gibt auch SpacesItemDecoration , mit dem der gewünschte Abstand zwischen allen RecyclerView-Elementen festgelegt wird. Funktioniert noch nicht mit StaggeredGridLayoutManager .

    Wie alles. Vielen Dank für Ihre Aufmerksamkeit.

    Jetzt auch beliebt: