In my android activity I am using a RecyclerView which contains number of MathViews. MathView is a third-party library which shows LaTeX contents (This
is somewhat similar to WebView. Implementation of MathView can be seen on this android project. github.com/lingarajsankaravelu/Katex).
The problem is, for rendering the content of this MathView, it takes a little bit longer time. As I have used few MathView components inside a RecycleView and the rendering time increases more. Therefore when the Activity started, at first in the view, some white space are shown for few seconds and then the relevant content is rendered.
As a solution for this problem, I need to show a progress bar until all the layout content of the Activity is completely rendered and after rendering is completed show up the Activity.
The relavent source codes are shown below.
MathView;
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:auto="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:id="#+id/equation_item"
android:clickable="true"
android:foreground="?attr/selectableItemBackground">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<katex.hourglass.in.mathlib.MathView
android:id="#+id/math_view"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="left"
app:setClickable="true"
app:setTextColor="#color/colorPrimary"
app:setTextSize="10sp"
/>
</android.support.v7.widget.CardView>
<include layout="#layout/item_divider"/>
</LinearLayout>
Recycler View;
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="com.a37moons.mathcheatsheets.ContentActivity"
tools:showIn="#layout/activity_content">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view_equations"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.NestedScrollView>
ViewHolder Class
public class ViewHolder extends RecyclerView.ViewHolder {
public MathView mathView;
public ViewHolder(View itemView) {
super(itemView);
mathView = itemView.findViewById(R.id.math_view);
}
}
Recycler Adapter Class
public class RecyclerAdapterEquations extends RecyclerView.Adapter<ViewHolder>{
private List<Equation> equations;
View view;
public RecyclerAdapterEquations(List<Equation> equations){
this.equations = equations;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.equation_view_item,parent,false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Equation sampleEquation = equations.get(position);
holder.mathView.setDisplayText(sampleEquation.equationString);
// holder.mathView2.setText(sampleEquation.equationString);
Log.d("MATH_APP","position "+position+" mathview text set ");
}
#Override
public int getItemCount() {
return equations.size();
}
}
Finally the implementation.
RecyclerView recyclerView;
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(new RecyclerAdapterEquations(sample));
With the help of the answer of written by azizbekian, I found the answer to my question.
As mentioned in that answer, this is the procedure;
Introduce a ProgressBar or something similar inside xml file. This view should be declared after RecyclerView in order to be
drawn on top of RecyclerView
Make RecyclerView invisible (via android:visibility="invisible")
Now RecyclerView will be actually laid out but not shown on the screen. You need a callback, that would be executed some time
later when RecyclerView is already setup. Within this callback you
will hide progress bar and change visibility of RecyclerView to
View.VISIBLE.
Now as the katex.hourglass.in.mathlib.MathView is a subclass of android WebView, we can set a WebChromeClient to this. Then, we can get the percentage of progress of loading the content.
int loadedPercentage = 0;
boolean loaded = false;
mathView.setWebChromeClient(new WebChromeClient(){
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
loadedPercentage = newProgress;
if(newProgress==100) {
//When the loading is 100% completed; todo
loaded = true;
Toast.makeText(getContext(), newProgress + "LOADED COMPLETELY", Toast.LENGTH_SHORT).show();
}
}
});
We can implement a progress bar to show the loadedPercentage. When the loadedPercentage is 100, we can see the relavent content is completely loaded.
So I editted the MathView class as follows;
public class MyMathView extends WebView {
private String TAG = "KhanAcademyKatexView";
private static final float default_text_size = 18;
private String display_text;
private int text_color;
private int text_size;
private boolean clickable = false;
private boolean loaded = false;
private int loadedPercentage = 0;
public MyMathView(Context context) {
//...
}
public MyMathView(Context context, AttributeSet attrs) {
//...
}
public boolean isLoaded(){
return loaded;
}
public int getLoadedPercentage() {
return loadedPercentage;
}
public void setViewBackgroundColor(int color)
{
//...
}
private void pixelSizeConversion(float dimension) {
//...
}
private void configurationSettingWebView()
{
//...
}
public void setDisplayText(String formula_text) {
this.display_text = formula_text;
loadData();
}
private String getOfflineKatexConfig()
{
String offline_config = "<!DOCTYPE html>\n" +
"<html>\n" +
" <head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Auto-render test</title>\n" +
" <link rel=\"stylesheet\" type=\"text/css\" href=\"file:///android_asset/katex/katex.min.css\">\n" +
" <link rel=\"stylesheet\" type=\"text/css\" href=\"file:///android_asset/themes/style.css\">\n" +
" <script type=\"text/javascript\" src=\"file:///android_asset/katex/katex.min.js\"></script>\n" +
" <script type=\"text/javascript\" src=\"file:///android_asset/katex/contrib/auto-render.min.js\"></script>\n" +
" <style type='text/css'>"+
"body {"+
"margin: 0px;"+
"padding: 0px;"+
"font-size:" +this.text_size+"px;"+
"color:"+getHexColor(this.text_color)+";"+
" }"+
" </style>"+
" </head>\n" +
" <body>\n" +
" {formula}\n" +
" <script>\n" +
" renderMathInElement(\n" +
" document.body\n" +
" );\n" +
" </script>\n" +
" </body>\n" +
"</html>";
String start = "<html><head><meta http-equiv='Content-Type' content='text/html' charset='UTF-8' /><style> body {"+
" white-space: nowrap;}</style></head><body>";
String end = "</body></html>";
//return start+offline_config.replace("{formula}",this.display_text)+end;
return offline_config.replace("{formula}",this.display_text);
}
private void loadData()
{
if (this.display_text!=null)
{
loadedPercentage = 0;
loaded = false;
this.setWebChromeClient(new WebChromeClient(){
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
loadedPercentage = newProgress;
if(newProgress==100) {
loaded = true;
Toast.makeText(getContext(), newProgress + "LOADED", Toast.LENGTH_SHORT).show();
}
}
});
this.loadDataWithBaseURL("null",getOfflineKatexConfig(),"text/html","UTF-8","about:blank");
}
}
public void setTextSize(int size)
{
//...
}
public void setTextColor(int color)
{
//...
}
private String getHexColor(int intColor)
{
//...
}
private void setDefaultTextColor(Context context) {
//...
}
private void setDefaultTextSize() {
//...
}
}
I cannot see the question itself in the description, thus I will refer to your statement in comments section:
I just need a solution to show a progress bar until all the layout content of the Activity is completely rendered and then after rendering is completed, show up the Activity.
Introduce a ProgressBar or something similar inside xml file. This view should be declared after RecyclerView in order to be drawn on top of RecyclerView
Make RecyclerView invisible (via android:visibility="invisible")
Now RecyclerView will be actually laid out but not shown on the screen. You need a callback, that would be executed some time later when RecyclerView is already setup. Within this callback you will hide progress bar and change visibility of RecyclerView to View.VISIBLE.
The problem boils to attaching "children of RecyclerView are initialized" listener.
As long as katex.hourglass.in.mathlib.MathView is a subclass of android.webkit.WebView, that means, that in order to get notified about load finished event you should register WebViewClient in following approach:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Equation sampleEquation = equations.get(position);
holder.mathView.setDisplayText(sampleEquation.equationString);
holder.mathView.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
// No longer interested in upcoming events - unregister
view.setWebViewClient(null);
// Now data is loaded, can make RecyclerView visible
}
});
...
}
Note, this will make RecyclerView to be shown as soon as any of the MathViews is loaded. You can wait all of them to be loaded depending on your requirement.
Don't call
setContentView(layout);
as usual, load data in adapter first, thn call
setContentView(layout);
After that set adapter to recyclerview .
Related
Lets say, in an Android app, we want to have the ability to temporarily and reliably ignore all user touches at any moment.
From the research I have done on stack-overflow as well as here, here, and here, the agreed-upon solution seems to be something like this:
(Code of MainActivity.java):
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (disableTouches) {
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
However, when we introduce the Android Monkey Exerciser Tool and send touch events to the app at a rapid rate, it becomes apparent that pigs begin to fly at the quantum level -- we can get calls to onClick() even after/during times where "blockTouches" has been set to true.
MY QUESTION IS: Why is that? -- Is this a normal Android behavior, or did I make a mistake in my code? :)
Note: I have already ruled out the possibility of onClick() being called by user input other than touches (and therefore being uncontrolled by the onDispatchTouchEvent() method)... by adding "—-pct-touch 100" to the monkey command.
Here is the code I am using for this test:
MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
View rootView; // turns black when "touch rejection" is in progress
View allowedButton;
View notAllowedButton;
// Used to decide whether to process touch events.
// Set true temporarily when notAllowedButton is clicked.
boolean touchRejectionAnimationInProgress = false;
int errorCount = 0; // counting "unexpected/impossible" click calls
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = findViewById(R.id.rootView);
allowedButton = findViewById(R.id.allowedButton);
notAllowedButton = findViewById(R.id.notAllowedButton);
allowedButton.setOnClickListener(this);
notAllowedButton.setOnClickListener(this);
allowedButton.setBackgroundColor(Color.GREEN);
notAllowedButton.setBackgroundColor(Color.RED);
}
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (touchRejectionAnimationInProgress) {
Log.i("XXX", "touch rejected in dispatchTouchevent()");
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
#Override
public void onClick(View viewThatWasClicked){
Log.i("XXX", "onClick() called. View clicked: " + viewThatWasClicked.getTag());
//checking for unexpected/"impossible"(?) calls to this method
if (touchRejectionAnimationInProgress) {
Log.i("XXX!", "IMPOSSIBLE(?) call to onClick() detected.");
errorCount ++;
Log.i("XXX!", "Number of unexpected clicks: " + errorCount);
return;
} // else proceed...
if (viewThatWasClicked == allowedButton) {
// Irrelevant
} else if (viewThatWasClicked == notAllowedButton) {
// user did something that is not allowed.
touchRejectionAnimation();
}
}
// When the user clicks on something "illegal,"
// all user input is ignored temporarily for 200 ms.
// (arbitrary choice of duration, but smaller is better for testing)
private void touchRejectionAnimation() {
Log.i("XXX", "touchRejectionAnimation() called.");
touchRejectionAnimationInProgress = true;
rootView.setBackgroundColor(Color.BLACK);
// for logging/debugging purposes...
final String rejectionID = (new Random().nextInt() % 9999999) + "";
Log.i("XXX", "rejection : " + rejectionID + " started.");
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
try { Thread.sleep(200); } catch (Exception e) {
Log.e("XXX", "exception in touchRejection() BG thread!");
}
runOnUiThread(new Runnable() {
#Override
public void run() {
Log.i("XXX", "rejection " + rejectionID + " ending");
rootView.setBackgroundColor(Color.WHITE);
touchRejectionAnimationInProgress = false;
}
});
}
});
thread.start();
}
}
layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="#+id/allowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="allowedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/notAllowedButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/notAllowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="view2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/allowedButton"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
If you don't want your onClick() to be triggered on any view click.
Following are the steps which need to take care.
Create custom viewGroup eg: MyConstraintLayout and add all child
inside it.
Override onInterceptTouchEvent(MotionEvent ev) and return it has true.
public class MyConstraintLayout extends ConstraintLayout {
private boolean mIsViewsTouchable;
public ParentView(Context context) {
super(context);
}
public ParentView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context, R.layout.custom_view, this);
}
public ParentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setViewsTouchable(boolean isViewTouchable) {
mIsViewsTouchable = isViewTouchable;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mIsViewsTouchable;
}
}
Note: Use setViewsTouchable() method as per your requirement, If you pass the parameter as true all views are not clickable if false your views will be clickable.
I have a basic layout setup: DrawerLayout that consists of the main layout for the activity and the layout for the navigation view. The main layout has a CoordinatorLayout and all the necessary ones to implement the collapsing toolbar functionality, as well as a ViewPager which I populate with fragments later on in the code. The fragment's layout mainly consists of the RecyclerView and other minor views.
The problem is the following: whenever I scroll either of the two tabs down a little and swipe to a new tab and return back to the original one, the RecyclerView receives the onScrolled callback (which it shouldn't since I haven't scrolled the RV itself, but the ViewPager) with the negative deltaY parameter (which means the direction is upwards) which causes the next item from the top to come into visibility as a result.
A couple of notes:
The deltaY parameter seems to be always equal to the next item's height from the top (which gives the impression that the RV scrolls one item upwards whenever this whole process happens)
I don't set the OnPageChangeListener to the ViewPager to get notified
whenever the page is scrolled, selected or the state was changed.
I supply the RecyclerViewScrollListener to the fragment's RecyclerView to get notified when the RV has been scrolled to do some logic (like propagating this event to the activity to show or hide the floating action button)
I've made a screenshot of the call stack when the onScrolled is called. Perhaps somebody might find it useful. Take a look.
I've also made a short video representing this behavior. Take a look.
I've checked a lot of resources on the Internet, but none of them helped me to resolve this issue. Feel free to ask any questions. Any help would be much appreciated.
dashboard_activity_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="#dimen/toolbar_shadow_height">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="#dimen/toolbar_height"
android:paddingTop="#dimen/toolbar_padding_top"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
app:titleTextAppearance="#style/TitleTextView"
tools:background="#color/colorPrimary"/>
<android.support.design.widget.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="#dimen/dashboard_activity_content_tab_layout_height"
app:tabMode="fixed"
app:tabGravity="fill"
tools:background="#color/colorPrimary"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:background="#color/colorPrimaryDark"/>
<android.support.design.widget.FloatingActionButton
android:id="#+id/actionButtonFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/dashboard_activity_content_fab_margin"
android:layout_gravity="bottom|end"
android:src="#mipmap/ic_search_white_24dp"
app:fabSize="normal"
app:useCompatPadding="true"
tools:backgroundTint="#color/colorAccent"/>
DashboardActivity.java (relevant parts):
private void initViewPager() {
mViewPager = findViewById(R.id.viewPager);
mAdapter = new DashboardViewPagerAdapter(getSupportFragmentManager());
mAdapter.setViewPagerId(mViewPager.getId());
mAdapter.setRecyclerViewStateListener(mRecyclerViewStateListener);
populateAdapter();
mViewPager.setAdapter(mAdapter);
mViewPager.setOffscreenPageLimit(mAdapter.getCount());
}
private void populateAdapter() {
BaseFragment baseFragment = mAdapter.getFragmentForPosition(TAB_TAB_1);
if(baseFragment == null) {
baseFragment = SomeFragment.newInstance(Common.SOME_TYPE_1);
}
mAdapter.addFragment(baseFragment);
baseFragment = mAdapter.getFragmentForPosition(TAB_TAB_2);
if(baseFragment == null) {
baseFragment = SomeFragment.newInstance(Common.SOME_TYPE_2);
}
mAdapter.addFragment(baseFragment);
baseFragment = mAdapter.getFragmentForPosition(TAB_TAB_3);
if(baseFragment == null) {
baseFragment = SomeOtherFragment.init();
}
mAdapter.addFragment(baseFragment);
}
private RecyclerViewStateListener mRecyclerViewStateListener = new RecyclerViewStateListener() {
#Override
public void onScrolledDownwards(RecyclerView recyclerView, int deltaY) {
// Hiding the FAB by animating it
DashboardCommon.hideActionButton(mActionButtonFab, mViewAnimator, true);
}
#Override
public void onScrolledUpwards(RecyclerView recyclerView, int deltaY) {
// Showing the FAB by animating it
DashboardCommon.showActionButton(mActionButtonFab, mViewAnimator, true);
}
};
SomeFragment.java (relevant parts):
private void initRecyclerView() {
mRecyclerView = findViewById(R.id.recyclerView);
mRecyclerView.addOnScrollListener(new RecyclerViewScrollListener(this));
mLayoutManager = new LinearLayoutManager(
getContext(),
LinearLayoutManager.VERTICAL,
false
);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new SomeRecyclerViewAdapter(getContext(), mItems);
mRecyclerView.setAdapter(mAdapter);
}
// mRecyclerViewStateListener is the listener passed from the
// DashboardActivity
#CallSuper
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onScrollStateChanged(recyclerView, newState);
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int deltaX, int deltaY) {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onScrolled(recyclerView, deltaX, deltaY);
}
}
#Override
public void onScrolledDownwards(RecyclerView recyclerView, int deltaY) {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onScrolledDownwards(recyclerView, deltaY);
}
}
#Override
public void onScrolledUpwards(RecyclerView recyclerView, int deltaY) {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onScrolledUpwards(recyclerView, deltaY);
}
}
#Override
public void onBottomReached() {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onBottomReached();
}
}
#Override
public void onMidpointReached(int direction) {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onMidpointReached(direction);
}
}
#Override
public void onTopReached() {
if(mRecyclerViewStateListener != null) {
mRecyclerViewStateListener.onTopReached();
}
}
RecyclerViewScrollListener.java (relevant parts):
public class RecyclerViewScrollListener extends RecyclerView.OnScrollListener {
public static final int DIRECTION_UNSPECIFIED = -1;
public static final int DIRECTION_UPWARDS = 0;
public static final int DIRECTION_DOWNWARDS = 1;
// Omitted...
private StateListener mStateListener;
public RecyclerViewScrollListener(StateListener stateListener) {
// Omitted..
mStateListener = stateListener;
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(mStateListener != null) {
mStateListener.onScrollStateChanged(recyclerView, newState);
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int deltaX, int deltaY) {
if(mStateListener != null) {
mStateListener.onScrolled(recyclerView, deltaX, deltaY);
}
f(deltaY > 0) {
// Recycler view's contents are moving downwards
// Notifying about the downwards scroll
if(mStateListener != null) {
mStateListener.onScrolledDownwards(recyclerView, deltaY);
}
// Omitted...
if(someConditionIsTrue) {
// Omitted...
// Notifying the listener
mStateListener.onBottomReached();
} else if((someOtherConditionIsTrue) {
mStateListener.onMidpointReached(DIRECTION_DOWNWARDS);
}
} else if(deltaY < 0) {
// Recycler view's contents are moving upwards
// Notifying about upwards scroll
if(mStateListener != null) {
mStateListener.onScrolledUpwards(recyclerView, deltaY);
}
// Omitted...
if(someConditionIsTrue) {
// Omitted..
// Notifying the listener
mStateListener.onTopReached();
} else if(someOtherConditionIsTrue) {
mStateListener.onMidpointReached(DIRECTION_UPWARDS);
}
}
}
public interface StateListener {
void onScrollStateChanged(RecyclerView recyclerView, int newState);
void onScrolled(RecyclerView recyclerView, int deltaX, int deltaY);
void onScrolledDownwards(RecyclerView recyclerView, int deltaY);
void onScrolledUpwards(RecyclerView recyclerView, int deltaY);
void onBottomReached();
void onMidpointReached(int direction);
void onTopReached();
}
}
Using This Scroll one item at a time automatically
RecyclerView my_recycler_view= (RecyclerView) findViewById(R.id.my_recycler_view);
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(my_recycler_view);
Happy Coding Cheers
Hope will help
RecyclerView my_recycler_view= (RecyclerView) findViewById(R.id.my_recycler_view);
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(my_recycler_view);
Even though I posted this question more than 2 years ago, I've decided to provide an answer. Better late than never, right?
As far as I remember, the problem was in the fact that the layout of the RecyclerView items had the TextView, which contained android:textIsSelectable="true" attribute. Turned out that by removing that attribute, the problem went away. How did I implement text selection then, you may ask? To be honest, I do not recall since it's been so long ago.
Anyway, if you have a problem like this, try removing the android:textIsSelectable="true" attribute and it may very well fix the issue for you.
I have a RecyclerView and add items to mCommentArrayList at index 0. I am trying to create a slide-in animation at the top of the view as new items (CardViews) are added to the RecyclerView.
I know there are libraries that can be used, and I have even explored https://github.com/wasabeef/recyclerview-animators. However, the documentation is limited and I am unsure what approach to take.
Note that I add all new items to my mCommentArrayList at index 0 so that they appear at the top of the view. I know there is some work to be done in the adapter, specifically onBindViewHolder(), but I don't know exactly what to put in order to activate the animations.
Where I first call to Firebase to find data to populate the RecyclerView:
mUpdateRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
setImage(dataSnapshot);
setQuestion(dataSnapshot);
createInitialCommentIDArray(dataSnapshot);
mNumberOfCommentsAtPoll = (int) dataSnapshot.child(COMMENTS_LABEL).getChildrenCount();
for (int i = 0; i < mNumberOfCommentsAtPoll; i++) {
String commentID = (String) dataSnapshot.child(COMMENTS_LABEL).child(mCommentIDArrayList.get(i)).child("COMMENT").getValue();
Log.v("COMMENT_ID", "The comment ID is " + commentID);
String userID = (String) dataSnapshot.child(COMMENTS_LABEL).child(mCommentIDArrayList.get(i)).child("USER_ID").getValue();
Log.v("USER_ID", "The user ID is " + userID);
mCommentArrayList.add(0, new Comments(mUserAvatar, userID, commentID));
mCommentAdapter.notifyDataSetChanged();
}
}
#Override
public void onCancelled(FirebaseError firebaseError) {
}
});
Subsequent Calls to Firebase on Data Change:
#Override
protected void onStart() {
super.onStart();
mUpdateComments = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
mNumberOfCommentsAtPoll = (int) dataSnapshot.getChildrenCount();
for (DataSnapshot x : dataSnapshot.child(COMMENTS_LABEL).getChildren()) {
Log.v("DATA_SNAPSHOT", x.toString());
if (mCommentIDArrayList.contains(x.getKey())) {
Log.v("Comment_Already_Added", x.getKey());
} else {
Log.v("Child_Added_Called", "Child Added Called");
mCommentIDArrayList.add(x.getKey());
String commentID = (String) dataSnapshot.child(COMMENTS_LABEL).child(x.getKey()).child("COMMENT").getValue();
Log.v("New_Comment", "The new comment is " + commentID);
String userID = (String) dataSnapshot.child(COMMENTS_LABEL).child(x.getKey()).child("USER_ID").getValue();
Log.v("New_User_ID", "The new userID is " + userID);
mCommentArrayList.add(0, new Comments(mUserAvatar, userID, commentID));
mPollCommentsList.getAdapter().notifyItemInserted(0);
}
}
}
Using the library you were talking about (https://github.com/wasabeef/recyclerview-animators) it's very easy to add a SlideInAnimator to your RecyclerView. Just use the following code to set an Animator to your RecyclerView (pick one):
recyclerView.setItemAnimator(new SlideInDownAnimator());
recyclerView.setItemAnimator(new SlideInRightAnimator());
recyclerView.setItemAnimator(new SlideInLeftAnimator());
recyclerView.setItemAnimator(new SlideInUpAnimator());
Once you have done this the you can simply trigger the animation by calling notifyItemInserted(position) or notifyItemRangeInserted(positionStart, itemCount). These calls will trigger the Animator, calling notifyDatasetChanged() won't.
Triggering the insertion animation:
recyclerView.getAdapter().notifyItemInserted(position);
recyclerView.getAdapter().notifyItemRangeInserted(positionStart, itemCount);
Hope this code help's you !
create one animation file animation_from_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="700"
android:fillAfter="false"
>
<translate
android:interpolator="#android:anim/decelerate_interpolator"
android:fromXDelta="100%p"
android:toXDelta="0"
/>
<alpha
android:fromAlpha="0.5"
android:toAlpha="1"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
/>
</set>
in Activity
Animation animation = AnimationUtils.loadAnimation(mActivity, R.anim.animation_from_right);
holder.itemView.startAnimation(animation);
use above code in your Adapter on onBindViewHolder
The best way animate the recyclerview will be to do it in the onbindviewholder method.
Here is how to do it-
create a field variable lastAnimatedPosition in the adapter class.
private int lastAnimatedPosition = -1;
Then in the onbindviewholder-
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Comments comment = mDataSet.get(position);
holder.userComment.setText(comment.getUserComment());
holder.userID.setText("User " + position);
if (position > lastAnimatedPosition) {
lastAnimatedPosition = position;
Animation animation = AnimationUtils.loadAnimation(context, R.anim.my_anim_set);
animation.setInterpolator(new AccelerateDecelerateInterpolator());
((ViewHolder) holder).container.setAnimation(animation);
animation.start();
}
}
Next a few tweaks in your viewholder class-
public class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
protected ImageView userAvatar;
protected TextView userID;
protected TextView userComment;
**protected View container;**
public ViewHolder(View v) {
super(v);
**container = v;**
userAvatar = (ImageView) v.findViewById(R.id.profile_image);
userID = (TextView) v.findViewById(R.id.user_ID);
userComment = (TextView) v.findViewById(R.id.user_comment_textview);
}
**public void clearAnimation() {
container.clearAnimation();
}**
}
And lastly simply override onViewDetachedFromWindow-
#Override
public void onViewDetachedFromWindow(final ViewHolder holder) {
holder.clearAnimation();
}
UPDATE
Since the element to be animated is in the 0th index replace the if (position > lastAnimatedPosition) snippet with -
if (position == 0) {
lastAnimatedPosition = position;
Animation animation = AnimationUtils.loadAnimation(context, R.anim.my_anim_set);
animation.setInterpolator(new AccelerateDecelerateInterpolator());
((ViewHolder) holder).container.setAnimation(animation);
animation.start();
}
I'm developing an Android app using Xamarin studio, but this is not important.
I use the pageViewer to upload the fragment (12 more or less). To the one display fragment i use a webView to display a local html page and with a swipe to the left, the webview content change to the next one.
So, at the fifth frgament memory problems started even if i used different tasks in my code.
My question is: Is there a way to 'detach' the fragment when i'm not displaying them? Can they not remain into my memory?
Thanks for all
This is my code, N.B: Java answers are accepeted as well
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView (Resource.Layout.BookView);
_loader = ProgressDialog.Show (this, "Loading...", "Please wait...", true);
//return chapters count
chapters = 12 //example
//var dp = (int)Resources.DisplayMetrics.Density;
_layout = FindViewById<DrawerLayout> (Resource.Id.drawer_layout);
_view = FindViewById<ViewPager>(Resource.Id.bView);
_view.SetBackgroundColor (Color.White);
_currentAdapter = new AwesomeFragmentAdapter (SupportFragmentManager, path, name, chapters, this, _view);
_view.Adapter = _currentAdapter;
_view.OffscreenPageLimit = chapters;
List<int> positions = new List<int> ();
_view.PageSelected += (object sender, ViewPager.PageSelectedEventArgs e) => {
//get details
var page_load = new Task (() => {
//return an object with the chapter details
_chap = object;
});
page_load.Start();
//find the webview
_web = (WebView)_view.FindViewWithTag(300 + e.Position);
WebSettings setting = _web.Settings;
setting.CacheMode = CacheModes.Default;
setting.JavaScriptEnabled = true;
setting.BuiltInZoomControls = true;
setting.DisplayZoomControls = false;
setting.PluginsEnabled = true;
setting.SetPluginState(WebSettings.PluginState.On);
//setting.JavaScriptCanOpenWindowsAutomatically = true;
if (positions.Contains(e.Position)) {
_web.ClearCache(true);
_web.ClearView();
}
//Start when the scroll is finished
_view.PageScrollStateChanged += (object sendero, ViewPager.PageScrollStateChangedEventArgs ex) => {
if (ex.State == 0 ) {
if (positions.Contains(e.Position)) {
//_web.Reload(); --> doesn't work
//Doesn't reload the .js animations
_web.LoadUrl ("file://" + path + "/" + _chap.Name);
} else {
_web.LoadUrl ("file://" + path + "/" + _chap.Name);
positions.Add(e.Position);
}
}
};
};
}
public class BWebClient : WebViewClient
{
int _position;
string _path;
Activity _parent;
ViewPager _pager;
string _chapName;
public BWebClient (int position, string Path, Activity Parent, ViewPager Pager, string ChapName){
_position = position;
_parent = Parent;
_path = Path;
_pager = Pager;
_chapName = ChapName;
}
public override void OnPageFinished (WebView view, string url)
{
base.OnPageFinished (view, url);
view.ScrollTo (0, _position);
}
public override bool ShouldOverrideUrlLoading (WebView view, string url)
{
if (url.StartsWith ("navigate")) {
string destination = url.Substring (url.IndexOf ("navigate://") + "navigate://".Length);
int DestinationChapter = Int32.Parse (destination.Substring (0, destination.IndexOf("_")));
int l = destination.IndexOf("_") + 1;
int b = destination.Length - l;
int DestinationPage = Int32.Parse (destination.Substring (l,b));
if (DestinationPage == 0) {
_pager.SetCurrentItem(DestinationChapter ,true);
WebView _web = (WebView)_pager.FindViewWithTag(300 + DestinationChapter);
_web.LoadUrl ("file://" + _path + "/" + _chapName);
}
} else if (url.StartsWith ("pdf")) {
string file_path = System.IO.Path.Combine (_path, url.Substring (url.IndexOf ("pdf://") + "pdf://".Length));
Android.Net.Uri pdfFile = Android.Net.Uri.FromFile (new Java.IO.File (file_path));
Intent pdfIntent = new Intent (Intent.ActionView);
pdfIntent.SetDataAndType (pdfFile, "application/pdf");
_parent.StartActivity (pdfIntent);
}
return true;
}
}
public class AwesomeFragmentAdapter : FragmentPagerAdapter
{
string _path;
string _filename;
int _chapters;
Activity _parent;
FileUtilities _fUtils;
ViewPager _pager;
public AwesomeFragmentAdapter (Android.Support.V4.App.FragmentManager fm,
string path,
string filename,
int chapters,
Activity parent,
FileUtilities FUtils,
ViewPager Pagers): base(fm)
{
_path = path;
_filename = filename;
_chapters = chapters;
_parent = parent;
_fUtils = FUtils;
_pager = Pagers;
}
public override int Count
{
/* --- return chapter count --- */
get { return _chapters;}
}
public override Android.Support.V4.App.Fragment GetItem(int _position)
{
/* --- get specific item --- */
return new AwesomeFragment (_path, _filename, _position, _parent, _fUtils, _pager);
}
}
public class AwesomeFragment : Android.Support.V4.App.Fragment
{
string _path;
WebView web_view;
string _filename;
int _position;
Activity _parent;
BanjiChapter _chap;
FileUtilities _fUtils;
ViewPager _pager;
public AwesomeFragment () {}
public AwesomeFragment (string path,
string filename,
int position,
Activity parent,
FileUtilities FUtils,
ViewPager Pager)
{
_path = path;
_filename = filename;
_position = position;
_parent = parent;
_fUtils = FUtils;
_pager = Pager;
}
public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
/* --- Create the view --- */
var view = inflater.Inflate (Resource.Layout.BookWebView, container, false);
//return the chapter
_chap = _fUtils.ReturnChapterDetails(_filename, _position);
web_view = view.FindViewById<WebView> (Resource.Id.webview);
web_view.SetWebViewClient(new BanjiWebClient(_position,_path,_parent, _pager, _chap.Name ));
web_view.SetBackgroundColor(Color.Transparent);
web_view.Settings.JavaScriptEnabled = true;
web_view.Tag = 300 + _position;
switch(Resources.DisplayMetrics.DensityDpi){
case Android.Util.DisplayMetricsDensity.Medium:
{
web_view.SetLayerType (LayerType.Software, null);
break;
}
case Android.Util.DisplayMetricsDensity.High:
{
web_view.SetLayerType (LayerType.Hardware, null);
break;
}
case Android.Util.DisplayMetricsDensity.Xhigh:
{
web_view.SetLayerType (LayerType.Hardware, null);
break;
}
}
if (_chap.Background == null) {
view.SetBackgroundColor (Color.White);
} else {
view.SetBackgroundDrawable (new BitmapDrawable (BitmapFactory.DecodeByteArray (_chap.Background, 0, _chap.Background.Length)));
}
if (_position == 0) {
web_view.LoadUrl ("file://" + _path + "/" + _chap.Name);
}
return view;
}
public BChapter GetCurrentBChapter()
{
return _chap;
}
}
EDIT:
BookView.axml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffececec">
<!-- The main content view -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/mainView">
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/bookView" />
<ImageButton
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="left|center"
android:background="#null"
android:id="#+id/menuButton"
/>
</FrameLayout>
<!-- The navigation drawer -->
<LinearLayout
android:id="#+id/left_menu"
android:layout_width="250dp"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:layout_gravity="start"
android:divider="#android:color/transparent"
android:dividerHeight="0dp"
android:background="#111">
<Button
android:id="#+id/backStep"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
style="#style/button_text"
android:background="#ff000000"
android:fitsSystemWindows="false" />
<Space
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/screllArea">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/ThumbLayout" />
</ScrollView>
</LinearLayout>
</android.support.v4.widget.DrawerLayout>
BookWebView
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Use a FragmentStatePagerAdapter instead; it is designed to minimise memory overhead by possibly destroying the fragment when it is not visible, saving only the state information of that fragment.
From the developers docs:
This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.
See here for documentation.
I'd like to have a TextView display text, and when you click/longclick on it, a textbox should "show up" and allow editing of said text. When you're done editing (onkey enter i suppose) it should revert back to a textview with the updated text...
I'm wondering if it's feasable to implement such a widget or should I hack a workaround? Tips and suggestions are very welcome.
If you need further idea of what I mean, just go to your e.g. (windows) skype profile and see for yourself.
EDIT:
Clarification: I'm specifically asking for a widget or such which is a textview until clicked on, then transforms to an edittext containing the same text; once done editing it transforms back to a textview representing the new changed text. Thats what i mean by "edittext on demand widget".
But I'm hoping to get something better than
public class Widget {
TextView text;
EditText edit;
String textToRepresent;
//...
}
You have a few different options here.
First you will have to register an onClick or onLongClick to the TextView that you want to make interactive. Just make sure that the user knows it's clickable
Then have your onClick function start a DialogFragment. I like to create show functions. Note that you can use the support libraries here to make your app backwards compatible.
private void showDialog() {
MyDialogFragment dialog = new MyDialogFragment();
dialog.show(getSupportFragmentManager(), "dialog");
}
The DialogFragment is pretty straight forward. In your onCreateView you'll inflate the View that you'll want to display to the user. You can alternatively wrap it with a simple AlertDialogBuilder if you don't want to go custom.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.your_dialog_layout);
mTitleEditText = (TextView) view.findViewById(R.id.title);
mTitleEditText.setOnClickListener(this);
return view;
}
After your findViewByIds set your onClickListeners.
The last thing you have to take care of is getting data back into your original TextView.
You can do this by creating a public method in your Activity that you can call from inside of your DialogFragment. Something like this
#Override
public void onClick(View v) {
int clickedId = v.getId();
if (clickedId == mDoneButton.getId()) {
MyActivity activity = (MyActivity)getActivity();
mTitle = mTitleEditText.getText().toString();
activity.setText(mTitle);
dismiss();
}
}
I would recommend using a DialogFragment because it will handle your life cycle nicely.
However, another option would be to create a new Activity themed to be a dialog
<activity android:theme="#android:style/Theme.Dialog" />
Then you can startActivityForResult to display your dialog and then capture your results in onActivityResult
Here is my solution. I just give you the basic one. Create a TextView in front of EditText and two Button OK,Cancel (You can change to ImageButton like Skype). Change the visiblity of two view. The code is so simple without comment. You can add some null checking according your logic.
public class CompoundTextView extends RelativeLayout implements OnClickListener {
private EditText edt;
private TextView txt;
RelativeLayout layout;
public SkypeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
edt = (EditText) findViewById(R.id.edt);
txt = (TextView) findViewById(R.id.txt_name);
layout = (RelativeLayout) findViewById(R.id.layout);
Button ok = (Button) findViewById(R.id.ok_btn);
Button cancel = (Button) findViewById(R.id.cancel_btn);
ok.setOnClickListener(this);
cancel.setOnClickListener(this);
txt.setOnClickListener(this);
}
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.ok_btn:
String editString = edt.getText().toString();
txt.setText(editString);
layout.setVisibility(View.INVISIBLE);
txt.setVisibility(View.VISIBLE);
break;
case R.id.cancel_btn:
layout.setVisibility(View.INVISIBLE);
txt.setVisibility(View.VISIBLE);
break;
case R.id.txt_name:
txt.setVisibility(View.INVISIBLE);
layout.setVisibility(View.VISIBLE);
break;
}
}
}
Create a XML skypetextview. You can customize font and background to make it's prettier.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="#+id/txt_name"
android:layout_width="fill_parent"
android:layout_height="100dp"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:background="#ff0000" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:id="#+id/layout" >
<EditText
android:id="#+id/edt"
android:layout_width="270dp"
android:layout_height="100dp" />
<Button
android:id="#+id/ok_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/edt"
android:text="OK" />
<Button
android:id="#+id/cancel_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/ok_btn"
android:layout_toRightOf="#id/edt"
android:text="Cancel" />
</RelativeLayout>
</RelativeLayout>
add (or include) this view to the layout you want.
Example :
public class TestActivity extends Activity {
SkypeTextView test;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflater inflate = getLayoutInflater();
test = (SkypeTextView ) inflate.inflate(R.layout.compound_text_view,
null);
setContentView(test);
}
PS: i forgot. You should add some underline format for your textview in order to make user notice it clickable
Let a EditText change its background based on its state(Editable or Frozen). Set a background selector that does this.
Use this selector xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_focused="true" android:drawable="#android:drawable/edit_text"/>
<item android:drawable="#android:drawable/screen_background_light_transparent"/>
</selector>
Like I said on thursday... Yul was pretty close but not quite close. He did have a general same idea but (theoretically) rushed into code too early ;)
The TextBoxOnDemand code supplied below is production-ready. The idea is similar to what I wanted to avoid in the OP and what Yul suggested, but with optimal implementation (using a ViewSwitcher instead of a RelativeLayout for instance)
I gathered the resources needed for this in the following articles:
Creating custom view from xml
Declaring a custom android UI element using XML
Defining custom attrs
How to pass custom component parameters in java and xml
http://kevindion.com/2011/01/custom-xml-attributes-for-android-widgets/
and decided to post them here because the official Google "training" docs are useless and are either obsolete (deprecated) or do not cover what I needed. I hope you don't mind me claiming my own bounty, but this is the solution I wanted (and expected, ergo the bounty).
I guess the code will have to do ;)
TextBoxOnDemand.java:
package com.skype.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
import android.text.util.Linkify;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnHoverListener;
import android.view.View.OnLongClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.ViewSwitcher;
import com.skype.ref.R;
import com.skype.ref.RemoteKeys;
public class TextBoxOnDemand extends ViewSwitcher implements OnClickListener, OnLongClickListener, OnFocusChangeListener, OnHoverListener,
OnEditorActionListener
{
public static final String LOGTAG = "TextBoxOnDemand";
private View btmGuard;
private ImageButton cancel, accept;
private EditText editor;
private RelativeLayout editorLayout;
private TextView face;
private String hint = new String();
private boolean inEditMode = false; //normally this is in textview mode
private boolean inputReady = false;
private String ourData = new String();
private String prefillData = new String();
private String tag = new String(); //usually tag is empty.
private View topGuard;
private int autoLinkMask;// = Linkify.EMAIL_ADDRESSES; //Linkify.ALL;
private ColorStateList textColor, hintColor = null;
public TextBoxOnDemand(Context context)
{
super(context);
build(context);
setEditable(false); //init
}
public TextBoxOnDemand(Context context, AttributeSet attrs)
{
super(context, attrs);
build(context);
init(context, attrs);
setEditable(false); //init
}
public String getPrefillData()
{
return prefillData;
}
public String getTag()
{
return tag;
}
public String getText()
{
Log.d(LOGTAG, "getText() returning '" + ourData + "'");
return ourData;
}
public boolean hasPrefillData()
{
return prefillData.isEmpty();
}
public boolean isEditable()
{
Log.d(LOGTAG, "isEditable() returning " + inEditMode);
return inEditMode;
}
#Override
public void onClick(View v)
{
Log.d(LOGTAG, "onClick(" + v + ")");
if (inEditMode)
{
if (v.equals(accept))
{
if (editor.getEditableText().length() == 0 || editor.getEditableText().length() > 5)
ourData = editor.getEditableText().toString();
setEditable(false);
} else if (v.equals(cancel))
{
setEditable(false);
}
}
}
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
// Log.d(LOGTAG, "onEditorAction(" + v + ", " + actionId + ", " + event + ") fired!");
Log.d(LOGTAG, "onEditorAction() fired, inputReady = " + inputReady);
if (editor.getEditableText().length() > 0 && editor.getEditableText().length() < (prefillData.length() + 2)) return true; //the user needs to enter something
if (inputReady && (event.getKeyCode() == RemoteKeys.ENTER.keycode() || event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) //always is
{
if (editor.getEditableText().length() > prefillData.length() || editor.getEditableText().length() == 0)
ourData = editor.getEditableText().toString();
setEditable(false);
return false;
}
if ((editor.getEditableText().toString().compareToIgnoreCase(ourData) == 0 || editor.getEditableText().toString()
.compareToIgnoreCase(prefillData) == 0)
&& !inputReady) //means we didn't just keep on holding enter
return true;
else
inputReady = true;
return true;
}
#Override
public void onFocusChange(View v, boolean hasFocus)
{
Log.d(LOGTAG, "onFocusChange(" + v + ", " + hasFocus + ")\tinEditMode = " + inEditMode);
if (inEditMode)
{
if (hasFocus && (v.equals(topGuard) || v.equals(btmGuard)))
{
setEditable(false);
requestFocus();
}
if (hasFocus && (v.equals(editor) || v.equals(accept) || v.equals(cancel)))
{
//do nothing, you should be able to browse freely here
if (ourData.isEmpty() && editor.getEditableText().length() < prefillData.length())
{
Log.d(LOGTAG, "adding prefill, before = " + editor.getEditableText());
editor.setText("");
editor.append(prefillData);
Log.d(LOGTAG, "now is = " + editor.getEditableText());
}
}
} else
{
String text = (ourData.isEmpty()) ? hint : ourData;
ColorStateList color;
if (hintColor != null && ourData.isEmpty())
color = hintColor;
else
color = textColor;
face.setTextColor(color);
if (hasFocus)
{
SpannableString ss = new SpannableString(text);
ss.setSpan(new UnderlineSpan(), 0, text.length(), 0);
face.setText(ss);
} else
face.setText(text);
}
}
#Override
public boolean onHover(View v, MotionEvent event)
{
// Log.d(LOGTAG, "onHover()");
String text = (ourData.isEmpty()) ? hint : ourData;
ColorStateList color;
if (hintColor != null && ourData.isEmpty())
color = hintColor;
else
color = textColor;
face.setTextColor(color);
switch (event.getAction())
{
case MotionEvent.ACTION_HOVER_ENTER:
SpannableString ss = new SpannableString(text);
ss.setSpan(new UnderlineSpan(), 0, text.length(), 0);
face.setText(ss);
break;
case MotionEvent.ACTION_HOVER_EXIT:
face.setText(text);
break;
}
return true;
}
#Override
public boolean onLongClick(View v)
{
Log.d(LOGTAG, "onLongClick()\tinEditMode = " + inEditMode);
if (!inEditMode) //implies that getDisplayedChild() == 0, meaning the textview
{
setEditable(true);
return true;
} else
return false;
}
public void setEditable(boolean value)
{
Log.d(LOGTAG, "setEditable(" + value + ")");
inEditMode = value;
if (inEditMode)
{
//display the editorLayout
face.setOnLongClickListener(null);
face.setOnHoverListener(null);
face.setOnFocusChangeListener(null); //because of GC.
face.setOnClickListener(null);
face.setVisibility(View.GONE);
setDisplayedChild(1);
editorLayout.setVisibility(View.VISIBLE);
editor.setOnFocusChangeListener(this);
editor.setOnEditorActionListener(this);
cancel.setOnClickListener(this);
accept.setOnClickListener(this);
accept.setOnFocusChangeListener(this);
cancel.setOnFocusChangeListener(this);
} else
{
editor.setOnFocusChangeListener(null);
editor.setOnEditorActionListener(null);
cancel.setOnClickListener(null);
accept.setOnClickListener(null);
accept.setOnFocusChangeListener(null);
cancel.setOnFocusChangeListener(null);
editorLayout.setVisibility(View.GONE);
setDisplayedChild(0);
face.setVisibility(View.VISIBLE);
face.setOnLongClickListener(this);
face.setOnHoverListener(this);
face.setOnFocusChangeListener(this);
face.setOnClickListener(this);
face.setFocusable(true);
face.setFocusableInTouchMode(true);
}
updateViews();
}
#Override
public void setNextFocusDownId(int nextFocusDownId)
{
super.setNextFocusDownId(nextFocusDownId);
face.setNextFocusDownId(nextFocusDownId);
// editor.setNextFocusDownId(nextFocusDownId);
accept.setNextFocusDownId(nextFocusDownId);
cancel.setNextFocusDownId(nextFocusDownId);
}
#Override
public void setNextFocusForwardId(int nextFocusForwardId)
{
super.setNextFocusForwardId(nextFocusForwardId);
face.setNextFocusForwardId(nextFocusForwardId);
editor.setNextFocusForwardId(nextFocusForwardId);
}
#Override
public void setNextFocusLeftId(int nextFocusLeftId)
{
super.setNextFocusLeftId(nextFocusLeftId);
face.setNextFocusLeftId(nextFocusLeftId);
editor.setNextFocusLeftId(nextFocusLeftId);
}
#Override
public void setNextFocusRightId(int nextFocusRightId)
{
super.setNextFocusRightId(nextFocusRightId);
face.setNextFocusRightId(nextFocusRightId);
cancel.setNextFocusRightId(nextFocusRightId);
}
#Override
public void setNextFocusUpId(int nextFocusUpId)
{
super.setNextFocusUpId(nextFocusUpId);
face.setNextFocusUpId(nextFocusUpId);
// editor.setNextFocusUpId(nextFocusUpId);
accept.setNextFocusUpId(nextFocusUpId);
cancel.setNextFocusUpId(nextFocusUpId);
}
public void setPrefillData(String prefillData)
{
this.prefillData = new String(prefillData);
}
public String setTag()
{
return tag;
}
public void setText(String text)
{
Log.d(LOGTAG, "setText(" + text + ")");
ourData = text;
updateViews();
}
private void build(Context context)
{
Log.d(LOGTAG, "build()");
addView(View.inflate(context, R.layout.textboxondemand, null));
setFocusable(true);
setFocusableInTouchMode(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setOnFocusChangeListener(this);
setOnLongClickListener(this);
face = (TextView) findViewById(R.id.TBOD_textview);
editorLayout = (RelativeLayout) findViewById(R.id.TBOD_layout);
editor = (EditText) findViewById(R.id.TBOD_edittext);
accept = (ImageButton) findViewById(R.id.TBOD_accept);
cancel = (ImageButton) findViewById(R.id.TBOD_cancel);
topGuard = (View) findViewById(R.id.TBOD_top);
btmGuard = (View) findViewById(R.id.TBOD_bottom);
face.setFocusable(true);
face.setFocusableInTouchMode(true);
face.setOnLongClickListener(this);
face.setOnHoverListener(this);
face.setOnFocusChangeListener(this);
face.setOnClickListener(this);
editor.setOnFocusChangeListener(this);
editor.setOnEditorActionListener(this);
editor.setHint(hint);
editor.setFocusable(true);
editor.setFocusableInTouchMode(true);
accept.setOnClickListener(this);
accept.setOnFocusChangeListener(this);
accept.setFocusable(true);
cancel.setFocusable(true);
cancel.setOnFocusChangeListener(this);
cancel.setOnClickListener(this);
topGuard.setFocusable(true);
topGuard.setOnFocusChangeListener(this);
btmGuard.setFocusable(true);
btmGuard.setOnFocusChangeListener(this);
editor.setNextFocusRightId(R.id.TBOD_accept);
editor.setNextFocusDownId(R.id.TBOD_bottom);
editor.setNextFocusUpId(R.id.TBOD_top);
accept.setNextFocusLeftId(R.id.TBOD_edittext);
accept.setNextFocusRightId(R.id.TBOD_cancel);
cancel.setNextFocusLeftId(R.id.TBOD_accept);
}
private void init(Context context, AttributeSet attrs)
{
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextBoxOnDemand);
//Use a
Log.d(LOGTAG, "init()");
if (a == null) Log.d(LOGTAG, "Did you include 'xmlns:app=\"http://schemas.android.com/apk/res-auto\"' in your root layout?");
final int N = a.getIndexCount();
for (int i = 0; i < N; ++i)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.TextBoxOnDemand_android_hint:
hint = new String(a.getString(attr));
editor.setHint(a.getString(attr));
break;
case R.styleable.TextBoxOnDemand_android_text:
ourData = new String(a.getString(attr));
break;
case R.styleable.TextBoxOnDemand_android_inputType:
int inputType = a.getInt(attr, -1);
if (inputType != -1) editor.setInputType(inputType);
break;
case R.styleable.TextBoxOnDemand_android_textColor:
textColor = a.getColorStateList(attr);
face.setTextColor(textColor);
break;
case R.styleable.TextBoxOnDemand_android_linksClickable:
face.setLinksClickable(a.getBoolean(attr, true));
break;
case R.styleable.TextBoxOnDemand_android_textColorHint:
hintColor = a.getColorStateList(attr);
break;
case R.styleable.TextBoxOnDemand_android_autoLink:
autoLinkMask = a.getInt(attr, 0);
face.setAutoLinkMask(autoLinkMask);
break;
default:
Log.d(LOGTAG, "Skipping attribute " + attr);
}
}
//Don't forget this
a.recycle();
}
private void updateViews()
{
Log.d(LOGTAG, "updateViews()");
// if (getDisplayedChild() == 0) //first child - textview
if (!inEditMode) //first child - textview
{
if (ourData.isEmpty())
{
if (hintColor != null) face.setTextColor(hintColor);
face.setText(hint);
} else
{
face.setTextColor(textColor);
face.setText(ourData);
}
face.setFocusable(true);
face.setFocusableInTouchMode(true);
face.setAutoLinkMask(autoLinkMask);
} else
{ //second child - edittext
editor.setFocusable(true);
editor.setFocusableInTouchMode(true);
if (ourData.startsWith(prefillData) || ourData.length() >= prefillData.length())
editor.setText("");
else
editor.setText(prefillData);
editor.append(ourData);
inputReady = false;
editor.requestFocus();
}
}
public void setAutoLinkMask(LinkifyEnum linkifyEnumConstant)
{
switch (linkifyEnumConstant)
{
case ALL:
autoLinkMask = Linkify.ALL;
break;
case EMAIL_ADDRESSES:
autoLinkMask = Linkify.EMAIL_ADDRESSES;
break;
case MAP_ADDRESSES:
autoLinkMask = Linkify.MAP_ADDRESSES;
break;
case PHONE_NUMBERS:
autoLinkMask = Linkify.PHONE_NUMBERS;
break;
case WEB_URLS:
autoLinkMask = Linkify.WEB_URLS;
break;
case NONE:
default:
autoLinkMask = 0;
break;
}
//set it now
face.setAutoLinkMask(autoLinkMask);
}
public enum LinkifyEnum
{
ALL, EMAIL_ADDRESSES, MAP_ADDRESSES, PHONE_NUMBERS, WEB_URLS, NONE
};
}
I'm still working out some focus-related issues but this works as intended. When I use onFocuslistener 1, you can't focus from one TextBox to the other; when the textbox itself is focusable, I can focus from one to the other just fine, but I cannot inter-focus thru children and thus can't focus on the edittext to type.
the XML file:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/TBOD_textview"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:autoLink="email"
android:focusable="true"
android:focusableInTouchMode="true"
android:linksClickable="true"
android:textAppearance="?android:attr/textAppearanceMedium" />
<RelativeLayout
android:id="#+id/TBOD_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<EditText
android:id="#+id/TBOD_edittext"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_below="#+id/TBOD_textview"
android:focusable="true"
android:focusableInTouchMode="true"
android:imeOptions="actionDone"
android:inputType="none"
android:maxLines="1"
android:padding="2dp"
android:singleLine="true"
android:textColor="#android:color/black"
android:textSize="14dp" />
<ImageButton
android:id="#+id/TBOD_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/TBOD_edittext"
android:layout_marginLeft="15dp"
android:layout_toRightOf="#+id/TBOD_edittext"
android:background="#drawable/button_accept_selector" />
<ImageButton
android:id="#+id/TBOD_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/TBOD_edittext"
android:layout_marginLeft="5dp"
android:layout_toRightOf="#+id/TBOD_accept"
android:background="#drawable/button_cancel_selector" />
<View
android:id="#+id/TBOD_top"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:background="#android:color/transparent" />
<View
android:id="#+id/TBOD_bottom"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:background="#android:color/transparent" />
</RelativeLayout>
</RelativeLayout>
and finally, the attrs.xml file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextBoxOnDemand">
<attr name="android:text" />
<attr name="android:inputType" />
<attr name="android:hint" />
<attr name="android:textColor" />
<attr name="android:textColorHint" />
<attr name="android:linksClickable" />
<attr name="android:autoLink" />
</declare-styleable>
</resources>
This is how I used it in my main xml (after including the required namespace add):
<com.shark.widget.TextBoxOnDemand
android:id="#+id/profile_email2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="#+id/profile_skypename"
android:layout_below="#+id/profile_email_placeholder"
android:hint="#string/add_email"
android:inputType="textEmailAddress"
android:textColor="#android:color/white"
android:textColorHint="#color/skype_blue" />
EDIT: I've debugged the focus issues. It turns out that giving focus to children is difficult unless you call
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
Which kinda remedies the issue but still doesn't solve it. After some while of playing around with the onFocusChange() listener still trying to get the perfect behaviour, I threw in the towel and put in added two focus guards. I realized I cannot track the loss of focus only on my container (due to it never receiving focus) but I might as well track the idea of wanting to move away from the edit field... So i went the dirty route and added two invisible bar-like views to sandwitch the edittext in between. Once they got the focus, I could hide the component and ensure they transition properly.
And there it is, now it works as it should. Thanks to all who participated.
EDIT3: final polished version, i dumped the custom tags because they simply don't work reliably enough. Lesson to be learned: if there is an android tag for something, don't bother cloning it.