android - How can I make sticky headers in RecyclerView? (Without external lib)

android - How can I make sticky headers in RecyclerView? (Without external lib)

Justice Nafiset Author: Justice Nafiset Date: 2022-08-15
android - How can I make sticky headers in RecyclerView? (Without external lib)

All you need to know about android - How can I make sticky headers in RecyclerView? (Without external lib) , in addintion to android - How can I make text in a TextView unclickable in the layout XML? , javascript - Why does Android click area have a wider radius than touchstart? How can I make it consistent? , security - Does Log4jshell make my android app vulnerable? If so, how can I protect my app , android - How can I make multiple startActivityForResult to other Activity?

  1. android - How can I make sticky headers in RecyclerView? (Without external lib)
  2. Question:

    I want to fix my header views in the top of the screen like in the image below and without using external libraries.

    enter image description here

    In my case, I don't want to do it alphabetically. I have two different types of views (Header and normal). I only want to fix to the top, the last header.


    Solution 1:

    Here I will explain how to do it without an external library. It will be a very long post, so brace yourself.

    First of all, let me acknowledge @tim.paetz whose post inspired me to set off to a journey of implementing my own sticky headers using ItemDecorations. I borrowed some parts of his code in my implementation.

    As you might have already experienced, if you attempted to do it yourself, it is very hard to find a good explanation of HOW to actually do it with the ItemDecoration technique. I mean, what are the steps? What is the logic behind it? How do I make the header stick on top of the list? Not knowing answers to these questions is what makes others to use external libraries, while doing it yourself with the use of ItemDecoration is pretty easy.

    Initial conditions

    1. You dataset should be a list of items of different type (not in a "Java types" sense, but in a "header/item" types sense).
    2. Your list should be already sorted.
    3. Every item in the list should be of certain type - there should be a header item related to it.
    4. Very first item in the list must be a header item.

    Here I provide full code for my RecyclerView.ItemDecoration called HeaderItemDecoration. Then I explain the steps taken in detail.

    public class HeaderItemDecoration extends RecyclerView.ItemDecoration {
    
     private StickyHeaderInterface mListener;
     private int mStickyHeaderHeight;
    
     public HeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface listener) {
      mListener = listener;
    
      // On Sticky Header Click
      recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
       public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
        if (motionEvent.getY() <= mStickyHeaderHeight) {
         // Handle the clicks on the header here ...
         return true;
        }
        return false;
       }
    
       public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
    
       }
    
       public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
       }
      });
     }
    
     @Override
     public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
      super.onDrawOver(c, parent, state);
    
      View topChild = parent.getChildAt(0);
      if (Util.isNull(topChild)) {
       return;
      }
    
      int topChildPosition = parent.getChildAdapterPosition(topChild);
      if (topChildPosition == RecyclerView.NO_POSITION) {
       return;
      }
    
      View currentHeader = getHeaderViewForItem(topChildPosition, parent);
      fixLayoutSize(parent, currentHeader);
      int contactPoint = currentHeader.getBottom();
      View childInContact = getChildInContact(parent, contactPoint);
      if (Util.isNull(childInContact)) {
       return;
      }
    
      if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
       moveHeader(c, currentHeader, childInContact);
       return;
      }
    
      drawHeader(c, currentHeader);
     }
    
     private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
      int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
      int layoutResId = mListener.getHeaderLayout(headerPosition);
      View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
      mListener.bindHeaderData(header, headerPosition);
      return header;
     }
    
     private void drawHeader(Canvas c, View header) {
      c.save();
      c.translate(0, 0);
      header.draw(c);
      c.restore();
     }
    
     private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
      c.save();
      c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
      currentHeader.draw(c);
      c.restore();
     }
    
     private View getChildInContact(RecyclerView parent, int contactPoint) {
      View childInContact = null;
      for (int i = 0; i < parent.getChildCount(); i++) {
       View child = parent.getChildAt(i);
       if (child.getBottom() > contactPoint) {
        if (child.getTop() <= contactPoint) {
         // This child overlaps the contactPoint
         childInContact = child;
         break;
        }
       }
      }
      return childInContact;
     }
    
     /**
      * Properly measures and layouts the top sticky header.
      * @param parent ViewGroup: RecyclerView in this case.
      */
     private void fixLayoutSize(ViewGroup parent, View view) {
    
      // Specs for parent (RecyclerView)
      int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
      int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
    
      // Specs for children (headers)
      int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
      int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);
    
      view.measure(childWidthSpec, childHeightSpec);
    
      view.layout(0, 0, view.getMeasuredWidth(), mStickyHeaderHeight = view.getMeasuredHeight());
     }
    
     public interface StickyHeaderInterface {
    
      /**
       * This method gets called by {@link HeaderItemDecoration} to fetch the position of the header item in the adapter
       * that is used for (represents) item at specified position.
       * @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
       * @return int. Position of the header item in the adapter.
       */
      int getHeaderPositionForItem(int itemPosition);
    
      /**
       * This method gets called by {@link HeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
       * @param headerPosition int. Position of the header item in the adapter.
       * @return int. Layout resource id.
       */
      int getHeaderLayout(int headerPosition);
    
      /**
       * This method gets called by {@link HeaderItemDecoration} to setup the header View.
       * @param header View. Header to set the data on.
       * @param headerPosition int. Position of the header item in the adapter.
       */
      void bindHeaderData(View header, int headerPosition);
    
      /**
       * This method gets called by {@link HeaderItemDecoration} to verify whether the item represents a header.
       * @param itemPosition int.
       * @return true, if item at the specified adapter's position represents a header.
       */
      boolean isHeader(int itemPosition);
     }
    }
    

    Business logic

    So, how do I make it stick?

    You don't. You can't make a RecyclerView's item of your choice just stop and stick on top, unless you are a guru of custom layouts and you know 12,000+ lines of code for a RecyclerView by heart. So, as it always goes with the UI design, if you can't make something, fake it. You just draw the header on top of everything using Canvas. You also should know which items the user can see at the moment. It just happens, that ItemDecoration can provide you with both the Canvas and information about visible items. With this, here are basic steps:

    1. In onDrawOver method of RecyclerView.ItemDecoration get the very first (top) item that is visible to the user.

          View topChild = parent.getChildAt(0);
      
    2. Determine which header represents it.

              int topChildPosition = parent.getChildAdapterPosition(topChild);
          View currentHeader = getHeaderViewForItem(topChildPosition, parent);
      
    3. Draw the appropriate header on top of the RecyclerView by using drawHeader() method.

    I also want to implement the behavior when the new upcoming header meets the top one: it should seem as the upcoming header gently pushes the top current header out of the view and takes his place eventually.

    Same technique of "drawing on top of everything" applies here.

    1. Determine when the top "stuck" header meets the new upcoming one.

              View childInContact = getChildInContact(parent, contactPoint);
      
    2. Get this contact point (that is the bottom of the sticky header your drew and the top of the upcoming header).

              int contactPoint = currentHeader.getBottom();
      
    3. If the item in the list is trespassing this "contact point", redraw your sticky header so its bottom will be at the top of the trespassing item. You achieve this with translate() method of the Canvas. As the result, the starting point of the top header will be out of visible area, and it will seem as "being pushed out by the upcoming header". When it is completely gone, draw the new header on top.

              if (childInContact != null) {
              if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
                  moveHeader(c, currentHeader, childInContact);
              } else {
                  drawHeader(c, currentHeader);
              }
          }
      

    The rest is explained by comments and thorough annotations in piece of code I provided.

    The usage is straight forward:

    mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
    

    Your mAdapter must implement StickyHeaderInterface for it to work. The implementation depends on the data you have.

    Finally, here I provide a gif with a half-transparent headers, so you can grasp the idea and actually see what is going on under the hood.

    Here is the illustration of "just draw on top of everything" concept. You can see that there are two items "header 1" - one that we draw and stays on top in a stuck position, and the other one that comes from the dataset and moves with all the rest items. The user won't see the inner-workings of it, because you'll won't have half-transparent headers.

    "just draw on top of everything" concept

    And here what happens in the "pushing out" phase:

    "pushing out" phase

    Hope it helped.

    Edit

    Here is my actual implementation of getHeaderPositionForItem() method in the RecyclerView's adapter:

    @Override
    public int getHeaderPositionForItem(int itemPosition) {
        int headerPosition = 0;
        do {
            if (this.isHeader(itemPosition)) {
                headerPosition = itemPosition;
                break;
            }
            itemPosition -= 1;
        } while (itemPosition >= 0);
        return headerPosition;
    }
    

    Slightly different implementation in Kotlin

    Solution 2:

    Easiest way is to just create an Item Decoration for your RecyclerView.

    import android.graphics.Canvas;
    import android.graphics.Rect;
    import android.support.annotation.NonNull;
    import android.support.v7.widget.RecyclerView;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    
    public class RecyclerSectionItemDecoration extends RecyclerView.ItemDecoration {
    
    private final int             headerOffset;
    private final boolean         sticky;
    private final SectionCallback sectionCallback;
    
    private View     headerView;
    private TextView header;
    
    public RecyclerSectionItemDecoration(int headerHeight, boolean sticky, @NonNull SectionCallback sectionCallback) {
        headerOffset = headerHeight;
        this.sticky = sticky;
        this.sectionCallback = sectionCallback;
    }
    
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
    
        int pos = parent.getChildAdapterPosition(view);
        if (sectionCallback.isSection(pos)) {
            outRect.top = headerOffset;
        }
    }
    
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c,
                         parent,
                         state);
    
        if (headerView == null) {
            headerView = inflateHeaderView(parent);
            header = (TextView) headerView.findViewById(R.id.list_item_section_text);
            fixLayoutSize(headerView,
                          parent);
        }
    
        CharSequence previousHeader = "";
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            final int position = parent.getChildAdapterPosition(child);
    
            CharSequence title = sectionCallback.getSectionHeader(position);
            header.setText(title);
            if (!previousHeader.equals(title) || sectionCallback.isSection(position)) {
                drawHeader(c,
                           child,
                           headerView);
                previousHeader = title;
            }
        }
    }
    
    private void drawHeader(Canvas c, View child, View headerView) {
        c.save();
        if (sticky) {
            c.translate(0,
                        Math.max(0,
                                 child.getTop() - headerView.getHeight()));
        } else {
            c.translate(0,
                        child.getTop() - headerView.getHeight());
        }
        headerView.draw(c);
        c.restore();
    }
    
    private View inflateHeaderView(RecyclerView parent) {
        return LayoutInflater.from(parent.getContext())
                             .inflate(R.layout.recycler_section_header,
                                      parent,
                                      false);
    }
    
    /**
     * Measures the header view to make sure its size is greater than 0 and will be drawn
     * https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
     */
    private void fixLayoutSize(View view, ViewGroup parent) {
        int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(),
                                                         View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(),
                                                          View.MeasureSpec.UNSPECIFIED);
    
        int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                                                       parent.getPaddingLeft() + parent.getPaddingRight(),
                                                       view.getLayoutParams().width);
        int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                                                        parent.getPaddingTop() + parent.getPaddingBottom(),
                                                        view.getLayoutParams().height);
    
        view.measure(childWidth,
                     childHeight);
    
        view.layout(0,
                    0,
                    view.getMeasuredWidth(),
                    view.getMeasuredHeight());
    }
    
    public interface SectionCallback {
    
        boolean isSection(int position);
    
        CharSequence getSectionHeader(int position);
    }
    }
    

    XML for your header in recycler_section_header.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/list_item_section_text"
        android:layout_width="match_parent"
        android:layout_height="@dimen/recycler_section_header_height"
        android:background="@android:color/black"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@android:color/white"
        android:textSize="14sp"
    />
    

    And finally to add the Item Decoration to your RecyclerView:

    RecyclerSectionItemDecoration sectionItemDecoration =
            new RecyclerSectionItemDecoration(getResources().getDimensionPixelSize(R.dimen.recycler_section_header_height),
                                              true, // true for sticky, false for not
                                              new RecyclerSectionItemDecoration.SectionCallback() {
                                                  @Override
                                                  public boolean isSection(int position) {
                                                      return position == 0
                                                          || people.get(position)
                                                                   .getLastName()
                                                                   .charAt(0) != people.get(position - 1)
                                                                                       .getLastName()
                                                                                       .charAt(0);
                                                  }
    
                                                  @Override
                                                  public CharSequence getSectionHeader(int position) {
                                                      return people.get(position)
                                                                   .getLastName()
                                                                   .subSequence(0,
                                                                                1);
                                                  }
                                              });
        recyclerView.addItemDecoration(sectionItemDecoration);
    

    With this Item Decoration you can either make the header pinned/sticky or not with just a boolean when creating the Item Decoration.

    You can find a complete working example on github: https://github.com/paetztm/recycler_view_headers

    Solution 3:

    I've made my own variation of Sevastyan's solution above

    class HeaderItemDecoration(recyclerView: RecyclerView, private val listener: StickyHeaderInterface) : RecyclerView.ItemDecoration() {
    
    private val headerContainer = FrameLayout(recyclerView.context)
    private var stickyHeaderHeight: Int = 0
    private var currentHeader: View? = null
    private var currentHeaderPosition = 0
    
    init {
        val layout = RelativeLayout(recyclerView.context)
        val params = recyclerView.layoutParams
        val parent = recyclerView.parent as ViewGroup
        val index = parent.indexOfChild(recyclerView)
        parent.addView(layout, index, params)
        parent.removeView(recyclerView)
        layout.addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        layout.addView(headerContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
    }
    
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
    
        val topChild = parent.getChildAt(0) ?: return
    
        val topChildPosition = parent.getChildAdapterPosition(topChild)
        if (topChildPosition == RecyclerView.NO_POSITION) {
            return
        }
    
        val currentHeader = getHeaderViewForItem(topChildPosition, parent)
        fixLayoutSize(parent, currentHeader)
        val contactPoint = currentHeader.bottom
        val childInContact = getChildInContact(parent, contactPoint) ?: return
    
        val nextPosition = parent.getChildAdapterPosition(childInContact)
        if (listener.isHeader(nextPosition)) {
            moveHeader(currentHeader, childInContact, topChildPosition, nextPosition)
            return
        }
    
        drawHeader(currentHeader, topChildPosition)
    }
    
    private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View {
        val headerPosition = listener.getHeaderPositionForItem(itemPosition)
        val layoutResId = listener.getHeaderLayout(headerPosition)
        val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
        listener.bindHeaderData(header, headerPosition)
        return header
    }
    
    private fun drawHeader(header: View, position: Int) {
        headerContainer.layoutParams.height = stickyHeaderHeight
        setCurrentHeader(header, position)
    }
    
    private fun moveHeader(currentHead: View, nextHead: View, currentPos: Int, nextPos: Int) {
        val marginTop = nextHead.top - currentHead.height
        if (currentHeaderPosition == nextPos && currentPos != nextPos) setCurrentHeader(currentHead, currentPos)
    
        val params = currentHeader?.layoutParams as? MarginLayoutParams ?: return
        params.setMargins(0, marginTop, 0, 0)
        currentHeader?.layoutParams = params
    
        headerContainer.layoutParams.height = stickyHeaderHeight + marginTop
    }
    
    private fun setCurrentHeader(header: View, position: Int) {
        currentHeader = header
        currentHeaderPosition = position
        headerContainer.removeAllViews()
        headerContainer.addView(currentHeader)
    }
    
    private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? =
            (0 until parent.childCount)
                .map { parent.getChildAt(it) }
                .firstOrNull { it.bottom > contactPoint && it.top <= contactPoint }
    
    private fun fixLayoutSize(parent: ViewGroup, view: View) {
    
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
    
        val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                parent.paddingLeft + parent.paddingRight,
                view.layoutParams.width)
        val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                parent.paddingTop + parent.paddingBottom,
                view.layoutParams.height)
    
        view.measure(childWidthSpec, childHeightSpec)
    
        stickyHeaderHeight = view.measuredHeight
        view.layout(0, 0, view.measuredWidth, stickyHeaderHeight)
    }
    
    interface StickyHeaderInterface {
    
        fun getHeaderPositionForItem(itemPosition: Int): Int
    
        fun getHeaderLayout(headerPosition: Int): Int
    
        fun bindHeaderData(header: View, headerPosition: Int)
    
        fun isHeader(itemPosition: Int): Boolean
    }
    }
    

    ... and here is implementation of StickyHeaderInterface (I did it directly in recycler adapter):

    override fun getHeaderPositionForItem(itemPosition: Int): Int =
        (itemPosition downTo 0)
            .map { Pair(isHeader(it), it) }
            .firstOrNull { it.first }?.second ?: RecyclerView.NO_POSITION
    
    override fun getHeaderLayout(headerPosition: Int): Int {
        /* ... 
          return something like R.layout.view_header
          or add conditions if you have different headers on different positions
        ... */
    }
    
    override fun bindHeaderData(header: View, headerPosition: Int) {
        if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0
        else /* ...
          here you get your header and can change some data on it
        ... */
    }
    
    override fun isHeader(itemPosition: Int): Boolean {
        /* ...
          here have to be condition for checking - is item on this position header
        ... */
    }
    

    So, in this case header is not just drawing on canvas, but view with selector or ripple, clicklistener, etc.

  3. android - How can I make text in a TextView unclickable in the layout XML?
  4. Question:

    I have different table rows, each of it contains some information text which should not be clickable and not selectable. But when I run this in the emulator, the text is always clickable.

    That means, when I click on any text block, its color changes to a dark grey. I don't want it to change. I want it to do nothing.

    Surely, I could set the dark grey to the text color so the user doesn't see that he clicks anything, but this is not what I want.

    I already tried different attributes as you can see in the example, but nothings help. Moreover, what do I actually have to set not clickable, the TableRow or the TextView inside the TableRow?

    Here is an example:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal"
        >
        <ScrollView 
                android:id="@+id/scrollView"
                android:layout_width="fill_parent" 
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical|center_horizontal">
        <TableLayout 
                android:layout_height="wrap_content"
                android:layout_width="fill_parent"
                android:stretchColumns="*"
                android:clickable="false">
            <TableRow
                android:layout_width="fill_parent"
                android:background="#777777"
                android:clickable="false">
                <TextView 
                     android:id="@+id/heading"
                     android:text="This is the cool heading"             
                     android:textColor="#FFFFFF"
                     android:layout_width="fill_parent"
                     android:textSize="14sp"
                     android:paddingLeft="5sp"
                     android:paddingRight="5sp"
                     android:paddingTop="2sp"
                     android:paddingBottom="2sp"
                     android:textStyle="bold" 
                     android:clickable="false"  
                     />
            </TableRow>
             <TableRow
                 android:clickable="false"
                 android:linksClickable="false"
                 android:focusable="false"
                 android:focusableInTouchMode="false">
                <TextView
                     android:text="This is example text. It should not be clickable, but it is."
                     android:textSize="14sp"
                     android:paddingLeft="5sp"
                     android:paddingRight="5sp"
                     android:paddingTop="2sp"
                     android:paddingBottom="2sp"
                     android:scrollHorizontally="false"
                     android:inputType="textMultiLine"
                     android:linksClickable="false"
                     android:clickable="false"
                     android:focusable="false"
                     android:focusableInTouchMode="false"
                     android:layout_width="0sp"
                     />
                   </TableRow>
                  </TableLayout>
                  </ScrollView>
                  </RelativeLayout>
    


    Solution 1:

    I found the solution, when I add android:longClickable="false" to the TextView, it works. And I only need those two settings in TextView in all:

    android:longClickable="false"
    android:clickable="false"
    

    Settings in TableRow are not needed.

    Solution 2:

    You need to put these attritbutes:

             android:clickable="false"
             android:linksClickable="false"
             android:focusable="false"
             android:focusableInTouchMode="false"
    

    In your textview element also. Right now you're just disabling the row, not the individual elements within the row.

  5. javascript - Why does Android click area have a wider radius than touchstart? How can I make it consistent?
  6. Question:

    I have a simple rectangular anchor tag. I used jQuery to respond to click and touchstart events with the following:

      $(document).ready(function() {
          $("#button").on("click touchstart", function(e) {
              $("#log").append(e.type + "<br/>");
          });
      });
    

    The HTML looks like this:

    <div id="wrapper">
      <a id="button" href="#">&nbsp;</a>
    </div>
    <div id="log">Log:<br></div>
    

    The CSS is simple:

      #wrapper {
          padding:50px;
      }
      #button {
          display:block;
          width:200px;
          height:40px;
          text-decoration:none;
          color:#333;
          background-color:#efefef;
          border:0px;
          padding:0px;
          margin:0px;
      }
    

    I built this as a demo to show the problem I'm talking about.

    When you tap the edge of the rectangular anchor, only the click event is fired. When you tap the center of the area, both click and touchstart are fired.

    Why is it that only click seems to be triggered with the fat-finger detection? Is there a way to make the touchstart event also work with fat fingers?


    Animation of problem

    Only click is fired on edge tap

    touch firing click only

    Expected behavior, both events

    touch firing both events


    Solution 1:

    The problem

    The click event on touch devices is meant to emulate a click based on tapping. There was a big problem here, because touch interfaces are significantly different from desktop interfaces. The biggest difference? Mouse clicks are a lot more precise than finger touches. To ensure that desktop sites and applications would at least be somewhat useful the behaviour you're observing was designed. That way a user on a mobile phone would still be capable of clicking a small link, even though his fingers are actually too imprecise to accurately click the link.

    The solution

    You aren't going to like this, but the solution is simply not to use click events on touchscreen devices. This is typically not done because the click event is actually triggered around 300ms after the touchEnd event and thus feels laggy either way (the delay is there to wait for a double tap) and now you have another reason to not use it.

    The hard part

    Devices that have both a touchscreen and a mouse. Your choice whether you want to bother with those. Personally I tend to just go with them emulating clicks and living with the extra lag whilst using touch events on mobile devices, but if you take the time you can create far more carefully crafted solutions probably.

  7. security - Does Log4jshell make my android app vulnerable? If so, how can I protect my app
  8. Question:

    Apache Log4J vulnerability https://www.csoonline.com/article/3644472/apache-log4j-vulnerability-actively-exploited-impacting-millions-of-java-based-apps.html is impacting a number of cloud services.

    I am using log4j in my Android application. How does it impact it or is there no impact at all? I am assuming since the malicious actor could only run local scripts, it should not be an issue but I wanted to confirm.


    Solution 1:

    Seems like Android apps are safe since JNDI isn't available on Android. I believe the following tweet was the first online mention of the vulnerability, and on the bottom of the second screenshot it says "Java's JNDI is not available on Android". https://web.archive.org/web/20211209230040/https://twitter.com/P0rZ9/status/1468949890571337731 (original tweet got deleted https://twitter.com/P0rZ9/status/1468949890571337731/photo/2)

  9. android - How can I make multiple startActivityForResult to other Activity?
  10. Question:

    In my MainActivity, I hava code like this:

    public void toSecondActivity(View v){
            if(condition1){
                Intent it = new Intent(MainActitivy.this,SecondAcitivity.class);
                //put extra
                ......
                startActivityForResult(it,1)
            }
            else if(condition2){
                Intent it = new Intent(MainActitivy.this,SecondAcitivity.class);
                //put extra
                ......
                startActivityForResult(it,2)
            }
        }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            // TODO Auto-generated method stub
            super.onActivityResult(requestCode, resultCode, data);
            if (resultCode == 3) {
            //do something
            }
            else if (resultCode == 4){
            //do some other thing
            }
    }
    

    Then in SecondActivity,I have this code:

    public void returnToMainAcitivity(View v){
                    Intent it = getIntent();
                    //put extra
                    .........
                    it.putExtra("ResourceID", mResourceId);
                    setResult(3, it);
                    finish();
    }
    

    My question is how can I set the result to 4 if it is started from condition2 with the same button?

    public void returnToMainAcitivity(View v){
                    Intent it = getIntent();
                    //put extra
                    .........
                    //Not 3 if started from condition2
                    //setResult(3, it);
                    setResult(4, it);
                    finish();
    }
    

    I'm pretty new to intent,so please teach me how to solve this problem.Thank you ;)


    Solution 1:

    If I have understood you correctly, try something like this for your conditions:

    Intent it = new Intent(MainActitivy.this,SecondAcitivity.class);
    it.PutExtra("StartedFromCondition",1)
    startActivityForResult(it,1)
    

    The just read the extra and return it as necessary.

    It should be noted that resultCode is usually either -1 (RESULT_CANCELED) or 0 (RESULT_OK) which indicate failure or success. You should really return extra data as Extras in the returned intent if you wish to pass data from the second activity back to the first.

    Solution 2:

    It just simple just pass one more parameter (inWhichCondition) in Intent & when you send the result back just put the value you are passed. Below is the sample code.

    public void toSecondActivity(View v){
            if(condition1){
                Intent it = new Intent(MainActitivy.this,SecondAcitivity.class);
                //put extra
                intent.PutExtra("inWhichCondition",3);
                startActivityForResult(it,1)
            }
            else if(condition2){
                Intent it = new Intent(MainActitivy.this,SecondAcitivity.class);
                //put extra
                intent.PutExtra("inWhichCondition",4);
                startActivityForResult(it,2)
            }
        }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            // TODO Auto-generated method stub
            super.onActivityResult(requestCode, resultCode, data);
            if (resultCode == 3) {
            //do something
            }
            else if (resultCode == 4){
            //do some other thing
            }
    }
    

    & when you set the Result back

    public void returnToMainAcitivity(View v){
                    Intent it = getIntent();
                    //put extra
    
                    **setResult(inWhichCondition, it);**
                    finish();
    }