Android Shared Element Transition Examples

0
User Management System
Learn Kotlin, Retrofit, MVVM and MySQL using this all-in-one app. It is designed to be beginner friendly.

According to android documentation,  shared elements transition determine how views that are shared between two activities transition between these activities. For example, if two activities have the same image in different positions and sizes, the changeImageTransform shared element transition translates and scales the image smoothly between these activities.

This thread looks at examples and libraries relating to Shared Transitions in android. Feel free to contribute more examples, links and libraries.

Easily Implement Shared Transition on Images

You can easily implement shared element transition on images using this library known as Transitional ImageView.

Let's show you how to.

Step 1 - Install the Library

First register jitpack as a repository in your app level build.gradle:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Then install the library:

implementation 'com.github.mostafaaryan:transitional-imageview:v0.2.2'

Step 2

Create Transitional ImageView in your layout by pasting the following code:

<com.mostafaaryan.transitionalimageview.TransitionalImageView
        android:id="@+id/transitional_image"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:scaleType="fitXY"
        android:adjustViewBounds="true"
        app:res_id="@drawable/sample_image" />

Step 3

Now build a TransitionImageObject and set it to the TransitionalImageView:

TransitionalImageView transitionalImageView = (TransitionalImageView) findViewById(R.id.transitional_image);
TransitionalImage transitionalImage = new TransitionalImage.Builder()
                                    .duration(500)
                                    .backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.color))
                                    .image(R.drawable.sample_image)
                                    /* or */
                                    .image(bitmap)
                                    .create();
transitionalImageView.setTransitionalImage(transitionalImage);

Full Example

Here is a beautiful example for using this library.

(a). Shoe.java

The model class to define a single shoe.

public class Shoe {

    private String Title;
    private String imageUrl;

    public Shoe(String title, String imageUrl) {
        Title = title;
        this.imageUrl = imageUrl;
    }

    public String getTitle() {
        return Title;
    }

    public String getImageUrl() {
        return imageUrl;
    }

}

(b). ShoeAdapter.java

Then the recyclerview adapter.

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.controller.MainActivity;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;


import java.io.IOException;
import java.util.ArrayList;

/**
 * Created by Mostafa Aryan Nejad on 8/11/17.
 */

public class ShoeAdapter extends RecyclerView.Adapter<ShoeAdapter.ViewHolder> {

    Context mContext;
    ArrayList<Shoe> shoes = new ArrayList<>();

    public ShoeAdapter(Context context, ArrayList<Shoe> shoes) {
        mContext = context;
        this.shoes = shoes;
    }


    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_shoe, parent, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) {

        final Shoe shoe = shoes.get(position);

        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                try{

                    final Bitmap bitmap = Picasso.with(mContext).load(shoe.getImageUrl()).get();
                    ((Activity) mContext).runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            TransitionalImage transitionalImage = new TransitionalImage.Builder()
                                    .duration(500)
                            /*.backgroundColor(ContextCompat.getColor(, R.color.colorAccent))*/
                                    /*.image(R.drawable.sample_image)*/
                                    .image(bitmap)
                                    .create();
                            holder.image.setTransitionalImage(transitionalImage);
                            bitmap.recycle();
                        }
                    });
                } catch (IOException e){e.printStackTrace();}
            }
        });


        holder.title.setText(shoe.getTitle());
        holder.sizes.setText("37,38,39,40");


    }

    @Override
    public int getItemCount() {
        return shoes.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {

        public TextView title;
        public TextView sizes;
        public TransitionalImageView image;


        public ViewHolder(View itemView) {
            super(itemView);

            title = (TextView) itemView.findViewById(R.id.shoe_title);
            sizes = (TextView) itemView.findViewById(R.id.shoe_sizes);
            image = (TransitionalImageView) itemView.findViewById(R.id.shoe_image);

        }

    }


}

(c). ShoeListActivity.java

The shoe list activity:

import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.TabLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;

import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.ariannejad.mostafa.transitional_imageview_implementation.adapter.ShoeAdapter;
import com.ariannejad.mostafa.transitional_imageview_implementation.model.Shoe;

import java.util.ArrayList;

public class ShoeListActivity extends AppCompatActivity {

    private RecyclerView shoeRecyclerView;
    private ArrayList<Shoe> shoes = new ArrayList<>();
    private ActionBar actionBar;
    private AppBarLayout appBarLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_shoe_list);

        shoeRecyclerView = (RecyclerView) findViewById(R.id.shoe_recycler_view);
        CollapsingToolbarLayout collapsingToolbar =
                (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
        appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
        setOnOffsetChangedListener();
        collapsingToolbar.setTitleEnabled(false);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        actionBar = getSupportActionBar();
        if(actionBar != null) actionBar.setTitle("");


        populateList();

    }

    private void populateList() {
        shoes.add(new Shoe("Skechers Relaxed Fit Empire Game On Walking Shoe",
                "https://www.shoes.com/pm/skech/skech800828_42965_hd2.jpg"));

        shoes.add(new Shoe("Skechers After Burn Memory Fit Geardo High Top Trainer",
                "https://www.shoes.com//pm/skech/skech798492_42965_hd2.jpg"));

        shoes.add(new Shoe("New Balance Fresh Foam Zante v3 Running Shoe",
                "https://www.shoes.com/pi/newba/hd/newba805216_436896_hd.jpg"));

        for(int i = 0 ; i <= 5 ; i++ ) {
            shoes.addAll(shoes);
        }

        displayList();
    }

    private void displayList() {
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
        RecyclerView.Adapter adapter = new ShoeAdapter(this, shoes);
        shoeRecyclerView.setLayoutManager(layoutManager);
        shoeRecyclerView.setAdapter(adapter);
    }

    private void setOnOffsetChangedListener() {

        appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {

            boolean isDisplayed = false;

            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                int totalScroll = appBarLayout.getTotalScrollRange();

                if (totalScroll + verticalOffset == 0) {
                    if (actionBar != null) {
                        actionBar.setTitle("Sneakers");
                    }
                    isDisplayed = true;
                } else if (isDisplayed) {
                    if (actionBar != null)
                        actionBar.setTitle("");
                    isDisplayed = false;
                }
            }

        });

    }


}

(d). MainActivity.java

And finally the main activity.

import android.content.Intent;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import com.ariannejad.mostafa.transitional_imageview_implementation.R;
import com.mostafaaryan.transitionalimageview.TransitionalImageView;
import com.mostafaaryan.transitionalimageview.model.TransitionalImage;
import com.squareup.picasso.Picasso;

import java.io.IOException;


public class MainActivity extends AppCompatActivity {

    private String imageUrl = "https://image.freepik.com/free-icon/android-logo_318-54237.jpg";
    TransitionalImageView tiv;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tiv = (TransitionalImageView) findViewById(R.id.sample_image);

        loadImage();

    }

    private void loadImage() {

        /*
        ImageLoader imageLoader;
        imageLoader = ImageLoader.getInstance();
        imageLoader.init(ImageLoaderConfiguration.createDefault(this));
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                DisplayImageOptions dio = new DisplayImageOptions.Builder()
                        .cacheInMemory(false).build();
                final Bitmap bmp = imageLoader.loadImageSync(imageUrl, dio);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tiv.setImage(bmp);
                    }
                });
            }
        });*/


        /* Glide.with(this).asBitmap().load(imageUrl).into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
                tiv.setImage(resource);
            }
        }); */

        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    final Bitmap b = Picasso.with(MainActivity.this).load(imageUrl).get();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
//                            tiv.setImage(b);
                            TransitionalImage transitionalImage = new TransitionalImage.Builder()
                                    .duration(500)
                                    .backgroundColor(ContextCompat.getColor(MainActivity.this, R.color.colorAccent))
                                    //.image(R.drawable.sample_image)
                                    .image(b)
                                    .create();
                            tiv.setTransitionalImage(transitionalImage);
                        }
                    });

                } catch (IOException e) {e.printStackTrace();}
            }
        });


    }


    public void onClickShoes(View view) {
        startActivity(new Intent(this, ShoeListActivity.class));
    }



}

Demo

Here is the demo of what you get when you run the project.

Android Shared Element Image Transition Example

Download

Here are the download links.

  1. Direct Download here.
  2. Follow Author here.

Kotlin Shared Transition RecyclerView and Fragments

This is also a simple shared transitions example written in Kotlin. This time round however a recyclerview is the shared element among two fragments.

Tools

Here are the things to keep in mind:

  • Programming Language - Kotlin
  • Minimum SDK - 21

1. Create Transitions

In  a folder known as transitions under resources add the following:

(a). change_bounds.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
    <changeBounds />
</transitionSet>

(b). change_image_transform.xml

Then:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
    <changeImageTransform />
</transitionSet>

2. Design Layouts

You will find the layouts in the code.

3. Write Code

Code is written in Kotlin in this case.

(a). Fragment1.kt

Here is the code for the first fragment.

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import android.transition.ChangeBounds
import android.transition.ChangeImageTransform


class Fragment1: Fragment() {

    private lateinit var lm: LinearLayoutManager

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.activity_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lm = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
            false)
        rv.layoutManager = lm
        val adapter = MainActivity.Adapter()
        rv.adapter = adapter
        btn.setOnClickListener {

//            val changeImageTransform =
//                TransitionInflater.from(context).inflateTransition(R.transition.change_image_transform)
//            val changeBoundsTransform =
//                TransitionInflater.from(context).inflateTransition(R.transition.change_bounds)

            sharedElementReturnTransition = ChangeBounds()
            sharedElementEnterTransition = ChangeImageTransform()
            exitTransition = ChangeBounds()

            val fragment2 = Fragment2()
            // Setup transition on second fragment
            fragment2.sharedElementEnterTransition = ChangeBounds()
            fragment2.enterTransition = ChangeBounds();

            val firstVisiblePosition = lm.findFirstVisibleItemPosition()
            val lastVisiblePosition = lm.findLastVisibleItemPosition()
            val transaction = fragmentManager!!.beginTransaction()
                .replace(R.id.container, fragment2, fragment2::class.java.simpleName)
                .addToBackStack("name")
            for (i in firstVisiblePosition..lastVisiblePosition) {
                val holderForAdapterPosition =
                    rv.findViewHolderForAdapterPosition(i) as MainActivity.Adapter.Holder
                val itemView = holderForAdapterPosition.itemView
                transaction.addSharedElement(itemView, "unique_key_$i")
            }
            transaction.commit()
        }
    }
}

(b). Fragment2.kt

Add the following code in second fragment.

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class Fragment2: Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.activity_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val lm = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL,
            false)
        rv.layoutManager = lm
        val adapter = MainActivity.Adapter()
        rv.adapter = adapter
        btn.setOnClickListener {
        }
        postponeEnterTransition()
    }

    override fun onStart() {
        super.onStart()
        rv.post {
            startPostponedEnterTransition()
        }
    }
}

(c). ScndActivity.kt

Then the second activity.

import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class ScndActivity: AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val lm = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,
            false)
        rv.layoutManager = lm
        val adapter = MainActivity.Adapter()
        rv.adapter = adapter
        btn.setOnClickListener {

            //            val currentOrientation = lm.orientation
//            if (currentOrientation == LinearLayoutManager.VERTICAL) {
//                lm.orientation = LinearLayoutManager.HORIZONTAL
//            } else {
//                lm.orientation = LinearLayoutManager.VERTICAL
//            }
//            adapter.notifyItemRangeChanged(1, adapter?.itemCount ?: 0)
        }
        supportPostponeEnterTransition()
        rv.post {
            supportStartPostponedEnterTransition()
        }
    }
}

(d). MainActivity.kt

And lastly the main activity,

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView


class MainActivity : AppCompatActivity() {

    private lateinit var lm: LinearLayoutManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.cont)
        val fragment1 = Fragment1()
        supportFragmentManager.beginTransaction()
            .add(R.id.container, fragment1, Fragment1::class.java.simpleName)
            .commit()
//        lm = LinearLayoutManager(this, LinearLayoutManager.VERTICAL,
//            false)
//        rv.layoutManager = lm
//        val adapter = Adapter()
//        rv.adapter = adapter
//        btn.setOnClickListener {
//            val firstVisiblePosition = lm.findFirstVisibleItemPosition()
//            val lastVisiblePosition = lm.findLastVisibleItemPosition()
//            val pairs = ArrayList<Pair<View, String>>()
//            for (i in firstVisiblePosition..lastVisiblePosition) {
//                val holderForAdapterPosition =
//                    rv.findViewHolderForAdapterPosition(i) as Adapter.Holder
//                val itemView = holderForAdapterPosition.itemView
//                pairs.add(Pair(itemView, "unique_key_$i"))
//            }
//            val bundle = ActivityOptions.makeSceneTransitionAnimation(
//                this,
//                *pairs.toTypedArray()
//            ).toBundle()
//            val fragment1 = Fragment1()
//            supportFragmentManager.beginTransaction()
//                .add(fragment1, Fragment1::class.java.simpleName)
//                .commit()
//            startActivity(Intent(this, ScndActivity::class.java), bundle)
//        }
    }

    override fun onResume() {
        super.onResume()
    }

    class Adapter : RecyclerView.Adapter<Adapter.Holder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
            Holder(
                LayoutInflater.from(parent.context).inflate(
                    R.layout.item_item,
                    parent,
                    false
                )
            )

        override fun getItemCount(): Int = 10

        override fun onBindViewHolder(holder: Holder, position: Int) {
            holder.bind(position)
        }

        class Holder(view: View) : RecyclerView.ViewHolder(view) {

            fun bind(position: Int) {
                itemView.transitionName = "unique_key_$position"
            }
        }
    }
}

Demo

Here is what you get when you run the project.

Android Kotlin Shared Transition Example RecyclerView

Download

  1. Direct Download here.
  2. Follow Author here.

Java Shared Transition with Fragments and FloatingActionButton

This is a simple one-class example to utilize a shared element transition within fragments in an android activity. The programming language is Java. While it is not written in androidx, you can easily update it to androidx fragments and it doesn't utilize any third partt library.

shared

Transitions

These are written in XML.  Tyically you create a transition resource directory and place the XML.

(a). shared_enter_transition.xml

Here is the code:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/default_anim_duration">

    <changeTransform/>
    <arcMotion
        android:minimumHorizontalAngle="0"
        android:minimumVerticalAngle="15"
        android:maximumAngle="90" />
    <changeBounds />

</transitionSet>

Activities

Here are the activities

(a). MainActivity.java

Here is the main activity:

import android.animation.Animator;
import android.app.Fragment;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.ActionBarActivity;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;

public class FabActivity extends ActionBarActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fab);

        getFragmentManager()
                .beginTransaction()
                .add(R.id.frag_content, TitleFragment.newInstance())
                .commit();
    }


    public static class TitleFragment extends Fragment {
        public static TitleFragment newInstance() {
            return new TitleFragment();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            final View view = inflater.inflate(R.layout.fragment_fab_title, container, false);
            final View fabbutton = view.findViewById(R.id.fab);
            fabbutton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    final ControlsFragment controlsFragment = ControlsFragment.newInstance();

                    setupSharedElementTransition(controlsFragment);
                    Fade f = new Fade();
                    f.setStartDelay(250);
                    setExitTransition(f);

                    getFragmentManager()
                            .beginTransaction()
                            .replace(R.id.frag_content, controlsFragment)
                            .addToBackStack("controls")
                            .addSharedElement(fabbutton, "pause_button")
                            .commit();
                }
            });
            return view;
        }

        private void setupSharedElementTransition(final ControlsFragment controlsFragment) {
            Transition sharedTransition = TransitionInflater.from(getActivity()).inflateTransition(R.transition.shared_enter_transition);
            controlsFragment.setSharedElementEnterTransition(sharedTransition);
            controlsFragment.setSharedElementReturnTransition(sharedTransition);
            sharedTransition.addListener(new Transition.TransitionListener() {
                @Override
                public void onTransitionEnd(Transition transition) {
                    controlsFragment.revealContent();
                }

                @Override
                public void onTransitionStart(Transition transition) {
                }

                @Override
                public void onTransitionCancel(Transition transition) {
                }

                @Override
                public void onTransitionPause(Transition transition) {
                }

                @Override
                public void onTransitionResume(Transition transition) {
                }
            });

        }


    }

    public static class ControlsFragment extends Fragment {

        public static ControlsFragment newInstance() {
            return new ControlsFragment();
        }


        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_fab_controls, container, false);
        }


        public void revealContent() {
            View layout = getView().findViewById(R.id.controls_layout);
            animateRevealColor(layout);
        }

        private void animateRevealColor(View targetView) {
            int cx = (targetView.getLeft() + targetView.getRight()) / 2;
            int cy = (targetView.getTop() + targetView.getBottom()) / 2;

            cx += targetView.getTranslationX();
            cy += targetView.getTranslationY();
            int finalRadius = Math.max(targetView.getWidth(), targetView.getHeight());

            Animator anim = ViewAnimationUtils.createCircularReveal(targetView, cx, cy, 0, finalRadius);
            targetView.setBackgroundColor(getResources().getColor(R.color.accent_material_light));
            anim.setDuration(getResources().getInteger(R.integer.default_anim_duration));
            anim.setInterpolator(new AccelerateInterpolator());
            anim.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationEnd(Animator animator) {
                    animateScaleButton(getView().findViewById(R.id.ff_button));
                    animateScaleButton(getView().findViewById(R.id.rew_button));
                }

                @Override
                public void onAnimationStart(Animator animator) {
                }

                @Override
                public void onAnimationCancel(Animator animator) {
                }

                @Override
                public void onAnimationRepeat(Animator animator) {
                }
            });
            anim.start();
        }

        private void animateScaleButton(View view) {
            ViewCompat.animate(view)
                    .scaleX(1)
                    .scaleY(1)
                    .setDuration(250)
                    .start();
        }
    }

}

Demo

Here is the demo of what you get when you run the project.

Fragments Shared Element Transition

Download Links

  1. Directly download the code here. (Please update it to androidx and reshare your code)
  2. Follow Author here.
Android MySQL Retrofit2 Multipart CRUD,Search,Pagination rating

When I was a 2nd year Software Engineering student, I buillt a now defunct online tool called Camposha(from Campus Share) using my then favorite language C#(ASP.NET) to compete OLX in my country(Kenya). The idea was to target campus students in Kenya. I got a few hundred signups but competing OLX proved too daunting. I decided to focus on my studies, learning other languages like Java,Python,Kotlin etc while meanwhile publishing tutorials at my YouTube Channel ProgrammingWizards TV which led to this site(camposha.info). Say hello or post me a suggestion: oclemmi@gmail.com . Follow me below; Github , and on my channel: ProgrammingWizards TV

We will be happy to hear your thoughts

Leave a reply

+ five = fifteen

Reset Password
Compare items
  • Total (0)
Compare
0
Shopping cart